Skip to main content

dynamodb_facade/operations/
mod.rs

1mod batch;
2mod delete;
3mod get;
4mod pagination;
5mod put;
6mod query;
7mod scan;
8mod transactions;
9mod type_state;
10mod update;
11
12pub use batch::*;
13pub use delete::*;
14pub use get::*;
15pub use pagination::*;
16pub use put::*;
17pub use query::*;
18pub use scan::*;
19pub use transactions::*;
20pub use type_state::*;
21pub use update::*;
22
23use serde::{Serialize, de::DeserializeOwned};
24use std::marker::PhantomData;
25
26use super::{
27    ApplyCondition, ApplyFilter, ApplyKeyCondition, ApplyProjection, ApplyUpdate,
28    AttributeDefinition, Condition, DynamoDBItem, HasAttribute, HasConstAttribute,
29    HasIndexKeyAttributes, IndexDefinition, Item, Key, KeyCondition, KeyConditionState, KeySchema,
30    PartitionKeyDefinition, Projection, Result, TableDefinition, Update,
31};
32
33// ---------------------------------------------------------------------------
34// DynamoDBItemOp trait — typed operation entry points
35// ---------------------------------------------------------------------------
36
37/// Primary entry point for typed single-item and collection CRUD operations.
38///
39/// This trait is **blanket-implemented** for every type that implements
40/// [`DynamoDBItem<TD>`]. You never implement it manually — implement
41/// `DynamoDBItem` (via the `dynamodb_item!` macro) and all methods here become
42/// available automatically.
43///
44/// Every method returns a builder with compile-time safety guarantees that
45/// mirror DynamoDB API constraints. For example, calling `.condition()` twice
46/// is a compile error (DynamoDB accepts one condition expression per request),
47/// and `.project()` automatically switches to raw output since projected
48/// results may be incomplete for deserialization.
49///
50/// # Operation overview
51///
52/// | Method | DynamoDB operation | Default return |
53/// |---|---|---|
54/// | [`get`][DynamoDBItemOp::get] | `GetItem` | `Option<T>` |
55/// | [`put`][DynamoDBItemOp::put] | `PutItem` | `()` |
56/// | [`delete`][DynamoDBItemOp::delete] | `DeleteItem` | `()` |
57/// | [`delete_by_id`][DynamoDBItemOp::delete_by_id] | `DeleteItem` | `Option<T>` (old) |
58/// | [`update`][DynamoDBItemOp::update] | `UpdateItem` | `()` |
59/// | [`update_by_id`][DynamoDBItemOp::update_by_id] | `UpdateItem` | `T` (new) |
60/// | [`scan`][DynamoDBItemOp::scan] | `Scan` | `Vec<T>` |
61/// | [`scan_index`][DynamoDBItemOp::scan_index] | `Scan` (LSI/GSI) | `Vec<T>` |
62/// | [`query`][DynamoDBItemOp::query] | `Query` | `Vec<T>` |
63/// | [`query_all`][DynamoDBItemOp::query_all] | `Query` (const PK) | `Vec<T>` |
64/// | [`query_index`][DynamoDBItemOp::query_index] | `Query` (LSI/GSI) | `Vec<T>` |
65/// | [`query_all_index`][DynamoDBItemOp::query_all_index] | `Query` (LSI/GSI, const PK) | `Vec<T>` |
66///
67/// # Examples
68///
69/// ```no_run
70/// # use dynamodb_facade::test_fixtures::*;
71/// use dynamodb_facade::{DynamoDBItemOp, KeyId, Update, Condition};
72///
73/// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
74/// # let client = cclient.clone();
75/// // Get a user by ID
76/// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1")).await?;
77///
78/// # let client = cclient.clone();
79/// // Get with consistent read
80/// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1"))
81///     .consistent_read()
82///     .await?;
83///
84/// # let client = cclient.clone();
85/// // Put a new user (unconditional)
86/// sample_user().put(client).await?;
87///
88/// # let client = cclient.clone();
89/// // Put a new user (create-only)
90/// sample_user().put(client).not_exists().await?;
91///
92/// # let client = cclient.clone();
93/// // Put with a custom condition
94/// sample_user()
95///     .put(client)
96///     .condition(User::not_exists() | Condition::lt("expiration_timestamp", 1_700_000_000))
97///     .await?;
98///
99/// # let client = cclient.clone();
100/// // Put and return the old item
101/// let old /* : Option<User> */ = sample_user().put(client).return_old().await?;
102///
103/// # let client = cclient.clone();
104/// // Delete an enrollment (unconditional)
105/// sample_enrollment().delete(client).await?;
106///
107/// # let client = cclient.clone();
108/// // Delete only if the item exists
109/// sample_enrollment().delete(client).exists().await?;
110///
111/// # let client = cclient.clone();
112/// // Delete with a custom condition
113/// sample_enrollment()
114///     .delete(client)
115///     .condition(Enrollment::exists() & Condition::not_exists("completed_at"))
116///     .await?;
117///
118/// # let client = cclient.clone();
119/// // Delete by ID and return the old item
120/// let old /* : Option<Enrollment> */ = Enrollment::delete_by_id(
121///     client,
122///     KeyId::pk("user-1").sk("course-42"),
123/// )
124/// .exists()
125/// .await?;
126///
127/// # let client = cclient.clone();
128/// // Update a user's role (fire-and-forget)
129/// sample_user()
130///     .update(client, Update::set("role", "instructor"))
131///     .exists()
132///     .await?;
133///
134/// # let client = cclient.clone();
135/// // Update by ID and return the updated item (default for update_by_id)
136/// let updated /* : User */ = User::update_by_id(
137///     client,
138///     KeyId::pk("user-1"),
139///     Update::set("name", "Bob"),
140/// )
141/// .exists()
142/// .await?;
143///
144/// # let client = cclient.clone();
145/// // Update with a custom condition
146/// let updated /* : User */ = User::update_by_id(
147///     client,
148///     KeyId::pk("user-1"),
149///     Update::set("role", "instructor"),
150/// )
151/// .condition(Condition::eq("role", "student"))
152/// .await?;
153///
154/// # let client = cclient.clone();
155/// // Update without returning the item
156/// User::update_by_id(
157///     client,
158///     KeyId::pk("user-1"),
159///     Update::set("name", "Bob"),
160/// )
161/// .exists()
162/// .return_none()
163/// .await?;
164///
165/// # let client = cclient.clone();
166/// // Query all enrollments for a user
167/// let enrollments /* : Vec<Enrollment> */ =
168///     Enrollment::query(client, Enrollment::key_condition("user-1"))
169///         .all()
170///         .await?;
171///
172/// # let client = cclient.clone();
173/// // Scan all users with a filter
174/// let instructors /* : Vec<User> */ = User::scan(client)
175///     .filter(Condition::eq("role", "instructor"))
176///     .all()
177///     .await?;
178/// # Ok(())
179/// # }
180/// ```
181pub trait DynamoDBItemOp<TD: TableDefinition>: DynamoDBItem<TD> {
182    /// Returns a [`GetItemRequest`] builder in `Typed` output mode for
183    /// fetching a single item by key.
184    ///
185    /// The returned builder can be `.await`ed directly (returns
186    /// `Option<T>`), or further configured with
187    /// [`.raw()`][GetItemRequest::raw],
188    /// [`.project()`][GetItemRequest::project], or
189    /// [`.consistent_read()`][GetItemRequest::consistent_read].
190    ///
191    /// # Examples
192    ///
193    /// ```no_run
194    /// # use dynamodb_facade::test_fixtures::*;
195    /// use dynamodb_facade::{DynamoDBItemOp, KeyId};
196    ///
197    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
198    /// # let client = cclient.clone();
199    /// // Simple get by ID
200    /// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1")).await?;
201    ///
202    /// # let client = cclient.clone();
203    /// // Consistent read
204    /// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1"))
205    ///     .consistent_read()
206    ///     .await?;
207    /// # Ok(())
208    /// # }
209    /// ```
210    fn get(
211        client: aws_sdk_dynamodb::Client,
212        key_id: Self::KeyId<'_>,
213    ) -> GetItemRequest<TD, Self, Typed>
214    where
215        Self: DeserializeOwned,
216    {
217        GetItemRequest::_new(client, Self::get_key_from_id(key_id))
218    }
219
220    /// Returns a [`PutItemRequest`] builder in `Typed` output mode with
221    /// `ReturnNothing` and no condition.
222    ///
223    /// The returned builder can be `.await`ed directly, or further configured
224    /// with [`.not_exists()`][PutItemRequest::not_exists],
225    /// [`.condition()`][PutItemRequest::condition],
226    /// [`.return_old()`][PutItemRequest::return_old], or
227    /// [`.raw()`][PutItemRequest::raw].
228    ///
229    /// # Panics
230    ///
231    /// Panics if serializing `self` via [`DynamoDBItem::to_item`] fails. See
232    /// [`DynamoDBItem::to_item`] for the conditions under which this can
233    /// happen — it is the caller's responsibility to provide a compatible
234    /// [`Serialize`] implementation.
235    ///
236    /// # Examples
237    ///
238    /// ```no_run
239    /// # use dynamodb_facade::test_fixtures::*;
240    /// use dynamodb_facade::{DynamoDBItemOp, Condition};
241    ///
242    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
243    /// let user = sample_user();
244    ///
245    /// # let client = cclient.clone();
246    /// // Unconditional put (overwrites any existing item)
247    /// user.put(client).await?;
248    ///
249    /// # let client = cclient.clone();
250    /// // Create-only: fails if item already exists
251    /// user.put(client).not_exists().await?;
252    ///
253    /// # let client = cclient.clone();
254    /// // Custom condition: create-only OR expired TTL
255    /// user.put(client)
256    ///     .condition(User::not_exists() | Condition::lt("expiration_timestamp", 1_700_000_000))
257    ///     .await?;
258    ///
259    /// # let client = cclient.clone();
260    /// // Put and return the old item
261    /// let old /* : Option<User> */ = user.put(client).return_old().await?;
262    /// # Ok(())
263    /// # }
264    /// ```
265    fn put(&self, client: aws_sdk_dynamodb::Client) -> PutItemRequest<TD, Self, Typed>
266    where
267        Self: Serialize,
268    {
269        PutItemRequest::_new(client, self.to_item())
270    }
271
272    /// Returns a [`DeleteItemRequest`] builder in `Typed` output mode with
273    /// `ReturnNothing` and no condition.
274    ///
275    /// The returned builder can be `.await`ed directly, or further configured
276    /// with [`.exists()`][DeleteItemRequest::exists],
277    /// [`.condition()`][DeleteItemRequest::condition],
278    /// [`.return_old()`][DeleteItemRequest::return_old], or
279    /// [`.raw()`][DeleteItemRequest::raw].
280    ///
281    /// # Examples
282    ///
283    /// ```no_run
284    /// # use dynamodb_facade::test_fixtures::*;
285    /// use dynamodb_facade::{DynamoDBItemOp, Condition};
286    ///
287    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
288    /// let enrollment = sample_enrollment();
289    ///
290    /// # let client = cclient.clone();
291    /// // Unconditional delete
292    /// enrollment.delete(client).await?;
293    ///
294    /// # let client = cclient.clone();
295    /// // Delete only if the item exists
296    /// enrollment.delete(client).exists().await?;
297    ///
298    /// # let client = cclient.clone();
299    /// // Delete with a custom condition
300    /// enrollment
301    ///     .delete(client)
302    ///     .condition(Enrollment::exists() & Condition::not_exists("completed_at"))
303    ///     .await?;
304    ///
305    /// # let client = cclient.clone();
306    /// // Delete and return the old item
307    /// let old /* : Option<Enrollment> */ = enrollment.delete(client).return_old().await?;
308    /// # Ok(())
309    /// # }
310    /// ```
311    fn delete(&self, client: aws_sdk_dynamodb::Client) -> DeleteItemRequest<TD, Self, Typed> {
312        DeleteItemRequest::_new(client, self.get_key())
313    }
314
315    /// Returns a [`DeleteItemRequest`] builder in `Typed` output mode with
316    /// `Return<Old>` and no condition.
317    ///
318    /// Unlike [`delete`][DynamoDBItemOp::delete], this method accepts a
319    /// `KeyId` instead of a loaded instance, and defaults to
320    /// `Return<Old>` — the deleted item is returned as `Option<T>`.
321    ///
322    /// The returned builder can be further configured with
323    /// [`.exists()`][DeleteItemRequest::exists],
324    /// [`.condition()`][DeleteItemRequest::condition],
325    /// [`.return_none()`][DeleteItemRequest::return_none], or
326    /// [`.raw()`][DeleteItemRequest::raw].
327    ///
328    /// # Examples
329    ///
330    /// ```no_run
331    /// # use dynamodb_facade::test_fixtures::*;
332    /// use dynamodb_facade::{DynamoDBItemOp, Condition, KeyId};
333    ///
334    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
335    /// # let client = cclient.clone();
336    /// // Simple delete by ID (returns the old item by default)
337    /// let old /* : Option<Enrollment> */ = Enrollment::delete_by_id(
338    ///     client,
339    ///     KeyId::pk("user-1").sk("course-42"),
340    /// )
341    /// .await?;
342    ///
343    /// # let client = cclient.clone();
344    /// // Delete only if the item exists
345    /// let old /* : Option<Enrollment> */ = Enrollment::delete_by_id(
346    ///     client,
347    ///     KeyId::pk("user-1").sk("course-42"),
348    /// )
349    /// .exists()
350    /// .await?;
351    ///
352    /// # let client = cclient.clone();
353    /// // Delete with a custom condition
354    /// let old /* : Option<Enrollment> */ = Enrollment::delete_by_id(
355    ///     client,
356    ///     KeyId::pk("user-1").sk("course-42"),
357    /// )
358    /// .condition(Enrollment::exists() & Condition::not_exists("completed_at"))
359    /// .await?;
360    ///
361    /// # let client = cclient.clone();
362    /// // Delete without returning the old item
363    /// Enrollment::delete_by_id(
364    ///     client,
365    ///     KeyId::pk("user-1").sk("course-42"),
366    /// )
367    /// .return_none()
368    /// .await?;
369    /// # Ok(())
370    /// # }
371    /// ```
372    fn delete_by_id(
373        client: aws_sdk_dynamodb::Client,
374        key_id: Self::KeyId<'_>,
375    ) -> DeleteItemRequest<TD, Self, Typed, Return<Old>>
376    where
377        Self: DeserializeOwned,
378    {
379        DeleteItemRequest::_new(client, Self::get_key_from_id(key_id)).return_old()
380    }
381
382    /// Returns an [`UpdateItemRequest`] builder in `Typed` output mode with
383    /// `ReturnNothing` and no condition.
384    ///
385    /// The returned builder can be `.await`ed directly, or further configured
386    /// with [`.exists()`][UpdateItemRequest::exists],
387    /// [`.condition()`][UpdateItemRequest::condition],
388    /// [`.return_new()`][UpdateItemRequest::return_new],
389    /// [`.return_old()`][UpdateItemRequest::return_old], or
390    /// [`.raw()`][UpdateItemRequest::raw].
391    ///
392    /// # Examples
393    ///
394    /// ```no_run
395    /// # use dynamodb_facade::test_fixtures::*;
396    /// use dynamodb_facade::{DynamoDBItemOp, Condition, Update};
397    ///
398    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
399    /// let user = sample_user();
400    ///
401    /// # let client = cclient.clone();
402    /// // Simple update (fire-and-forget)
403    /// user.update(client, Update::set("role", "instructor")).await?;
404    ///
405    /// # let client = cclient.clone();
406    /// // Update guarded by existence
407    /// user.update(client, Update::set("role", "instructor"))
408    ///     .exists()
409    ///     .await?;
410    ///
411    /// # let client = cclient.clone();
412    /// // Update with a custom condition
413    /// user.update(client, Update::set("role", "instructor"))
414    ///     .condition(Condition::eq("role", "student"))
415    ///     .await?;
416    ///
417    /// # let client = cclient.clone();
418    /// // Update if exist and return the new item
419    /// let updated /* : User */ = user
420    ///     .update(client, Update::set("name", "Alice B."))
421    ///     .exists()
422    ///     .return_new()
423    ///     .await?;
424    /// # Ok(())
425    /// # }
426    /// ```
427    fn update(
428        &self,
429        client: aws_sdk_dynamodb::Client,
430        update: Update<'_>,
431    ) -> UpdateItemRequest<TD, Self, Typed> {
432        UpdateItemRequest::_new(client, self.get_key(), update)
433    }
434
435    /// Returns an [`UpdateItemRequest`] builder in `Typed` output mode with
436    /// `Return<New>` and no condition.
437    ///
438    /// Unlike [`update`][DynamoDBItemOp::update], this method accepts a
439    /// `KeyId` instead of a loaded instance, and defaults to `Return<New>` —
440    /// the updated item is returned as `T`.
441    ///
442    /// The returned builder can be further configured with
443    /// [`.exists()`][UpdateItemRequest::exists],
444    /// [`.condition()`][UpdateItemRequest::condition],
445    /// [`.return_none()`][UpdateItemRequest::return_none],
446    /// [`.return_old()`][UpdateItemRequest::return_old], or
447    /// [`.raw()`][UpdateItemRequest::raw].
448    ///
449    /// # Examples
450    ///
451    /// ```no_run
452    /// # use dynamodb_facade::test_fixtures::*;
453    /// use dynamodb_facade::{DynamoDBItemOp, Condition, KeyId, Update};
454    ///
455    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
456    /// # let client = cclient.clone();
457    /// // Simple update by ID (returns the updated item by default)
458    /// let updated /* : User */ = User::update_by_id(
459    ///     client,
460    ///     KeyId::pk("user-1"),
461    ///     Update::set("role", "instructor"),
462    /// )
463    /// .await?;
464    ///
465    /// # let client = cclient.clone();
466    /// // Update guarded by existence
467    /// let updated /* : User */ = User::update_by_id(
468    ///     client,
469    ///     KeyId::pk("user-1"),
470    ///     Update::set("role", "instructor"),
471    /// )
472    /// .exists()
473    /// .await?;
474    ///
475    /// # let client = cclient.clone();
476    /// // Update with a custom condition
477    /// let updated /* : User */ = User::update_by_id(
478    ///     client,
479    ///     KeyId::pk("user-1"),
480    ///     Update::set("role", "instructor"),
481    /// )
482    /// .condition(Condition::eq("role", "student"))
483    /// .await?;
484    ///
485    /// # let client = cclient.clone();
486    /// // Update without returning the item
487    /// User::update_by_id(
488    ///     client,
489    ///     KeyId::pk("user-1"),
490    ///     Update::set("name", "Bob"),
491    /// )
492    /// .exists()
493    /// .return_none()
494    /// .await?;
495    /// # Ok(())
496    /// # }
497    /// ```
498    fn update_by_id(
499        client: aws_sdk_dynamodb::Client,
500        key_id: Self::KeyId<'_>,
501        update: Update<'_>,
502    ) -> UpdateItemRequest<TD, Self, Typed, Return<New>>
503    where
504        Self: DeserializeOwned,
505    {
506        UpdateItemRequest::_new(client, Self::get_key_from_id(key_id), update)
507    }
508
509    /// Returns a [`ScanRequest`] builder in `Typed` output mode for scanning
510    /// the entire table.
511    ///
512    /// The returned builder can be executed with
513    /// [`.all()`][ScanRequest::all] or [`.stream()`][ScanRequest::stream],
514    /// and further configured with [`.filter()`][ScanRequest::filter],
515    /// [`.project()`][ScanRequest::project], [`.limit()`][ScanRequest::limit],
516    /// or [`.raw()`][ScanRequest::raw].
517    ///
518    /// Prefer [`query`][DynamoDBItemOp::query] when possible — scans read every
519    /// item in the table and are significantly more expensive.
520    ///
521    /// Also, note that this method will fail in most cases because a DynamoDB table
522    /// rarely contains only items of the same type. Use
523    /// [`scan_index`][DynamoDBItemOp::scan_index] to scan an index that may contain
524    /// only a specific type of item, or [`.raw()`][ScanRequest::raw] to prevent item
525    /// deserialization.
526    ///
527    /// # Examples
528    ///
529    /// ```no_run
530    /// # use dynamodb_facade::test_fixtures::*;
531    /// use dynamodb_facade::{DynamoDBItemOp, Condition};
532    ///
533    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
534    /// # let client = cclient.clone();
535    /// // Scan the table and attempts projecting all items as users
536    /// let all_users /* : Vec<User> */ = User::scan(client).all().await?;
537    ///
538    /// # let client = cclient.clone();
539    /// // Scan with a filter (prefer using query on an appropriate index)
540    /// let instructors /* : Vec<User> */ = User::scan(client)
541    ///     .filter(Condition::eq("role", "instructor"))
542    ///     .all()
543    ///     .await?;
544    /// # Ok(())
545    /// # }
546    /// ```
547    fn scan(client: aws_sdk_dynamodb::Client) -> ScanRequest<TD, Self, Typed>
548    where
549        Self: DeserializeOwned,
550    {
551        ScanRequest::_new(client)
552    }
553
554    /// Returns a [`ScanRequest`] builder in `Typed` output mode for scanning
555    /// a secondary index (GSI or LSI).
556    ///
557    /// `I` must be an [`IndexDefinition`] for `TD`, and `Self` must implement
558    /// [`HasAttribute<A>`] for evey keys of the IndexDefinition to confirm the
559    /// type participates in that index.
560    ///
561    /// Also, note that this method will fail is the index does not contains only items
562    /// of the expected type. Use [`.raw()`][ScanRequest::raw] to prevent item
563    /// deserialization.
564    ///
565    /// # Examples
566    ///
567    /// ```no_run
568    /// # use dynamodb_facade::test_fixtures::*;
569    /// use dynamodb_facade::{DynamoDBItemOp, Condition};
570    ///
571    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
572    /// # let client = cclient.clone();
573    /// // Scan an index and attempts projecting all items as enrollments
574    /// let all_enrollments /* : Vec<Enrollment> */ =
575    ///     Enrollment::scan_index::<TypeIndex>(client)
576    ///         .all()
577    ///         .await?;
578    ///
579    /// # let client = cclient.clone();
580    /// // Scan an index with a filter (prefer using query on an appropriate index)
581    /// let recent /* : Vec<Enrollment> */ =
582    ///     Enrollment::scan_index::<TypeIndex>(client)
583    ///         .filter(Condition::gt("enrolled_at", 1_700_000_000))
584    ///         .all()
585    ///         .await?;
586    /// # Ok(())
587    /// # }
588    /// ```
589    fn scan_index<I: IndexDefinition<TD>>(
590        client: aws_sdk_dynamodb::Client,
591    ) -> ScanRequest<TD, Self, Typed>
592    where
593        Self: DeserializeOwned + HasIndexKeyAttributes<TD, I>,
594    {
595        ScanRequest::_new_index::<I>(client)
596    }
597
598    /// Returns a [`QueryRequest`] builder in `Typed` output mode for querying
599    /// the table with the given key condition.
600    ///
601    /// The returned builder can be executed with
602    /// [`.all()`][QueryRequest::all] or [`.stream()`][QueryRequest::stream],
603    /// and further configured with [`.filter()`][QueryRequest::filter],
604    /// [`.project()`][QueryRequest::project], [`.limit()`][QueryRequest::limit],
605    /// [`.reverse()`][QueryRequest::reverse], or
606    /// [`.raw()`][QueryRequest::raw].
607    ///
608    /// # Examples
609    ///
610    /// ```no_run
611    /// # use dynamodb_facade::test_fixtures::*;
612    /// use dynamodb_facade::{DynamoDBItemOp, Condition, KeyCondition};
613    ///
614    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
615    /// # let client = cclient.clone();
616    /// // Query all enrollments for a specific user
617    /// let enrollments /* : Vec<Enrollment> */ = Enrollment::query(
618    ///     client,
619    ///     Enrollment::key_condition("user-1").sk_begins_with("ENROLL#"),
620    /// )
621    /// .all()
622    /// .await?;
623    ///
624    /// # let client = cclient.clone();
625    /// // Query with a filter
626    /// let advanced /* : Vec<Enrollment> */ =
627    ///     Enrollment::query(client, Enrollment::key_condition("user-1"))
628    ///         .filter(Condition::gt("progress", 0.5))
629    ///         .all()
630    ///         .await?;
631    /// # Ok(())
632    /// # }
633    /// ```
634    fn query(
635        client: aws_sdk_dynamodb::Client,
636        key_condition: KeyCondition<'_, TD::KeySchema, impl KeyConditionState>,
637    ) -> QueryRequest<TD, Self, Typed>
638    where
639        Self: DeserializeOwned,
640    {
641        QueryRequest::_new(client, key_condition)
642    }
643
644    /// Returns a [`QueryRequest`] builder in `Typed` output mode, using the
645    /// type's constant partition key value as the key condition.
646    ///
647    /// Available only when `Self` has a compile-time constant value for the
648    /// table's partition key (i.e. implements
649    /// `HasConstAttribute<TD::KeySchema::PartitionKey>`).
650    ///
651    /// # Examples
652    ///
653    /// ```no_run
654    /// # use dynamodb_facade::test_fixtures::*;
655    /// use dynamodb_facade::DynamoDBItemOp;
656    ///
657    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
658    /// // Query all items stored under the constant PlatformConfig PK
659    /// let configs /* : Vec<PlatformConfig> */ = PlatformConfig::query_all(client)
660    ///     .all()
661    ///     .await?;
662    /// # Ok(())
663    /// # }
664    /// ```
665    fn query_all(client: aws_sdk_dynamodb::Client) -> QueryRequest<TD, Self, Typed>
666    where
667        Self: DeserializeOwned + HasConstAttribute<<TD::KeySchema as KeySchema>::PartitionKey>,
668    {
669        Self::query(client, KeyCondition::pk(Self::VALUE))
670    }
671
672    /// Returns a [`QueryRequest`] builder in `Typed` output mode for querying
673    /// a secondary index (GSI or LSI) with the given key condition.
674    ///
675    /// `I` must be an [`IndexDefinition`] for `TD`, and `Self` must implement
676    /// [`HasAttribute<A>`] for evey keys of the IndexDefinition. The key
677    /// condition is typed to the index's key schema, preventing mismatched
678    /// attribute usage at compile time.
679    ///
680    /// # Examples
681    ///
682    /// ```no_run
683    /// # use dynamodb_facade::test_fixtures::*;
684    /// use dynamodb_facade::{DynamoDBItemOp, KeyCondition};
685    ///
686    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
687    /// // Query a secondary index
688    /// let users /* : Vec<User> */ = User::query_index::<EmailIndex>(
689    ///     client,
690    ///     KeyCondition::pk("alice@example.com".to_owned()),
691    /// )
692    /// .all()
693    /// .await?;
694    /// # Ok(())
695    /// # }
696    /// ```
697    fn query_index<I: IndexDefinition<TD>>(
698        client: aws_sdk_dynamodb::Client,
699        key_condition: KeyCondition<'_, I::KeySchema, impl KeyConditionState>,
700    ) -> QueryRequest<TD, Self, Typed>
701    where
702        Self: DeserializeOwned + HasIndexKeyAttributes<TD, I>,
703    {
704        QueryRequest::_new_index::<I>(client, key_condition)
705    }
706
707    /// Returns a [`QueryRequest`] builder in `Typed` output mode for querying
708    /// a secondary index (GSI or LSI) using the type's constant PK value for
709    /// that index.
710    ///
711    /// Available only when `Self` has a compile-time constant value for the
712    /// index's partition key (i.e. implements
713    /// `HasConstAttribute<I::KeySchema::PartitionKey>`).
714    ///
715    /// # Examples
716    ///
717    /// ```no_run
718    /// # use dynamodb_facade::test_fixtures::*;
719    /// use dynamodb_facade::DynamoDBItemOp;
720    ///
721    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
722    /// // Query all users via the TypeIndex (constant ItemType = "USER")
723    /// let all_users /* : Vec<User> */ = User::query_all_index::<TypeIndex>(client)
724    ///     .all()
725    ///     .await?;
726    /// # Ok(())
727    /// # }
728    /// ```
729    fn query_all_index<I: IndexDefinition<TD>>(
730        client: aws_sdk_dynamodb::Client,
731    ) -> QueryRequest<TD, Self, Typed>
732    where
733        Self: DeserializeOwned
734            + HasIndexKeyAttributes<TD, I>
735            + HasConstAttribute<<I::KeySchema as KeySchema>::PartitionKey>,
736    {
737        Self::query_index::<I>(client, KeyCondition::pk(Self::VALUE))
738    }
739
740    // -- Condition helpers ----------------------------------------------------
741
742    /// Returns a [`Condition`] that checks whether an item exists.
743    ///
744    /// Generates `attribute_exists(<PK>)` using the table's partition key
745    /// attribute name. Useful as a guard on put, delete, and update operations,
746    /// or as a component in compound conditions.
747    ///
748    /// See also `exists` shorthand methods on the individual request builders.
749    ///
750    /// # Examples
751    ///
752    /// ```
753    /// # use dynamodb_facade::test_fixtures::*;
754    /// use dynamodb_facade::{DynamoDBItemOp, Condition};
755    ///
756    /// // Use as a standalone condition
757    /// let cond = User::exists();
758    ///
759    /// // Combine with another condition using `&`
760    /// let cond = User::exists() & Condition::eq("role", "admin");
761    /// ```
762    fn exists() -> Condition<'static> {
763        Condition::exists(<TD::KeySchema as KeySchema>::PartitionKey::NAME)
764    }
765
766    /// Returns a [`Condition`] that checks whether an item does not exist.
767    ///
768    /// Generates `attribute_not_exists(<PK>)` using the table's partition key
769    /// attribute name. Commonly used with [`put`][DynamoDBItemOp::put] to
770    /// implement create-only semantics.
771    ///
772    /// See also `not_exists` shorthand methods on the individual request builders.
773    ///
774    /// # Examples
775    ///
776    /// ```
777    /// # use dynamodb_facade::test_fixtures::*;
778    /// use dynamodb_facade::{DynamoDBItemOp, Condition};
779    ///
780    /// // Use as a standalone condition
781    /// let cond = User::not_exists();
782    ///
783    /// // Combine: create-only OR expired TTL
784    /// let cond = User::not_exists() | Condition::lt("expiration_timestamp", 1_700_000_000);
785    /// ```
786    fn not_exists() -> Condition<'static> {
787        Condition::not_exists(<TD::KeySchema as KeySchema>::PartitionKey::NAME)
788    }
789
790    // -- Key Condition helpers ------------------------------------------------
791
792    /// Builds a [`KeyCondition`] for the table's partition key from a typed ID.
793    ///
794    /// Uses the type's [`HasAttribute`] implementation to convert `pk_id` into
795    /// the DynamoDB attribute value for the partition key. The resulting
796    /// condition can be extended with sort-key constraints before being passed
797    /// to [`query`][DynamoDBItemOp::query] methods.
798    ///
799    /// # Examples
800    ///
801    /// ```
802    /// # use dynamodb_facade::test_fixtures::*;
803    /// use dynamodb_facade::DynamoDBItemOp;
804    ///
805    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
806    /// // All enrollments for a user
807    /// let kc = Enrollment::key_condition("user-1").sk_begins_with("ENROLL#");
808    /// let enrollments /* : Vec<Enrollment> */ = Enrollment::query(client, kc)
809    ///     .all()
810    ///     .await?;
811    /// # Ok(())
812    /// # }
813    /// ```
814    fn key_condition(
815        pk_id: <Self as HasAttribute<PartitionKeyDefinition<TD>>>::Id<'_>,
816    ) -> KeyCondition<'static, TD::KeySchema> {
817        KeyCondition::pk(<Self as HasAttribute<PartitionKeyDefinition<TD>>>::attribute_value(pk_id))
818    }
819
820    /// Builds a [`KeyCondition`] for a secondary index's partition key from a typed ID.
821    ///
822    /// Uses the type's [`HasAttribute`] implementation for the index's
823    /// partition key attribute to convert `pk_id` into the appropriate
824    /// DynamoDB value. The resulting condition is typed to the index's key
825    /// schema, so sort-key methods are only available when the index has a
826    /// sort key.
827    ///
828    /// # Examples
829    ///
830    /// ```
831    /// # use dynamodb_facade::test_fixtures::*;
832    /// use dynamodb_facade::DynamoDBItemOp;
833    ///
834    /// // Build a key condition for the EmailIndex
835    /// let kc = User::index_key_condition::<EmailIndex>("alice@example.com");
836    /// let _ = kc;
837    /// ```
838    fn index_key_condition<I: IndexDefinition<TD>>(
839        pk_id: <Self as HasAttribute<<I::KeySchema as KeySchema>::PartitionKey>>::Id<'_>,
840    ) -> KeyCondition<'static, I::KeySchema>
841    where
842        Self: HasAttribute<<I::KeySchema as KeySchema>::PartitionKey>,
843    {
844        KeyCondition::pk(<Self as HasAttribute<
845            <I::KeySchema as KeySchema>::PartitionKey,
846        >>::attribute_value(pk_id))
847    }
848}
849
850impl<TD: TableDefinition, DBI: DynamoDBItem<TD>> DynamoDBItemOp<TD> for DBI {}