Skip to main content

dynamodb_facade/operations/
scan.rs

1use super::*;
2
3use aws_sdk_dynamodb::operation::scan::builders::ScanFluentBuilder;
4
5/// Builder for a DynamoDB `Scan` request.
6///
7/// Constructed via [`DynamoDBItemOp::scan`] / [`DynamoDBItemOp::scan_index`]
8/// (typed, with a concrete `T`) or [`ScanRequest::new`] /
9/// [`ScanRequest::new_index`] (stand-alone, raw output). The builder provides:
10///
11/// - **Output format** — the result can be deserialized into `T`.
12///   Call [`.raw()`][ScanRequest::raw] to receive untyped [`Item<TD>`]
13///   values instead (one-way). Calling [`.project()`][ScanRequest::project]
14///   also forces raw output.
15/// - **Filter** — call [`.filter()`][ScanRequest::filter] to add a
16///   server-side filter expression. DynamoDB accepts a single filter
17///   expression per request, so this can only be called once.
18/// - **Projection** — call [`.project()`][ScanRequest::project] to limit
19///   which attributes are returned. This can only be called once.
20///
21/// Use [`.all()`][ScanRequest::all] to collect all pages into a `Vec`, or
22/// [`.stream()`][ScanRequest::stream] for lazy page-by-page iteration.
23///
24/// **Note:** Scans read every item in the table (or index) and are
25/// significantly more expensive than queries. Prefer [`QueryRequest`] when
26/// possible.
27///
28/// # Errors
29///
30/// Returns [`Err`] if any DynamoDB page request fails or if deserialization
31/// of any returned item fails.
32///
33/// # Examples
34///
35/// ```no_run
36/// # use dynamodb_facade::test_fixtures::*;
37/// use dynamodb_facade::{DynamoDBItemOp, Condition};
38///
39/// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
40/// # let client = cclient.clone();
41/// // Simple scan
42/// let all_users /* : Vec<User> */ = User::scan(client).all().await?;
43///
44/// # let client = cclient.clone();
45/// // Scan with a filter
46/// let instructors /* : Vec<User> */ = User::scan(client)
47///     .filter(Condition::eq("role", "instructor"))
48///     .all()
49///     .await?;
50/// # Ok(())
51/// # }
52/// ```
53#[must_use = "builder does nothing until executed via .all() or .stream()"]
54pub struct ScanRequest<
55    TD: TableDefinition,
56    T = (),
57    O: OutputFormat = Raw,
58    F: FilterState = NoFilter,
59    P: ProjectionState = NoProjection,
60> {
61    builder: ScanFluentBuilder,
62    _marker: PhantomData<(TD, T, O, F, P)>,
63}
64
65// -- Stand-alone constructors (T = (), O = Raw)
66
67impl<TD: TableDefinition> ScanRequest<TD> {
68    /// Creates a stand-alone `ScanRequest` against the full table with raw output.
69    ///
70    /// Output is raw (`T = ()`, `O = Raw`). For typed access, prefer
71    /// [`DynamoDBItemOp::scan`] instead.
72    ///
73    /// # Examples
74    ///
75    /// ```no_run
76    /// # use dynamodb_facade::test_fixtures::*;
77    /// use dynamodb_facade::ScanRequest;
78    ///
79    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
80    /// let items = ScanRequest::<PlatformTable>::new(client).all().await?;
81    /// # Ok(())
82    /// # }
83    /// ```
84    pub fn new(client: aws_sdk_dynamodb::Client) -> Self {
85        Self::_new(client)
86    }
87
88    /// Creates a stand-alone `ScanRequest` scoped to a secondary index.
89    ///
90    /// Output is raw (`T = ()`, `O = Raw`). For typed access, prefer
91    /// [`DynamoDBItemOp::scan_index`] instead.
92    ///
93    /// # Examples
94    ///
95    /// ```no_run
96    /// # use dynamodb_facade::test_fixtures::*;
97    /// use dynamodb_facade::ScanRequest;
98    ///
99    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
100    /// let items = ScanRequest::<PlatformTable>::new_index::<TypeIndex>(client)
101    ///     .all()
102    ///     .await?;
103    /// # Ok(())
104    /// # }
105    /// ```
106    pub fn new_index<I: IndexDefinition<TD>>(client: aws_sdk_dynamodb::Client) -> Self {
107        Self::_new_index::<I>(client)
108    }
109}
110
111// -- Common methods (all states) --------------------------------------------
112
113impl<TD: TableDefinition, T, O: OutputFormat, F: FilterState, P: ProjectionState>
114    ScanRequest<TD, T, O, F, P>
115{
116    pub(super) fn _new(client: aws_sdk_dynamodb::Client) -> Self {
117        let table_name = TD::table_name();
118        tracing::debug!(table_name, "Scan");
119        Self {
120            builder: client.scan().table_name(table_name),
121            _marker: PhantomData,
122        }
123    }
124
125    pub(super) fn _new_index<I: IndexDefinition<TD>>(client: aws_sdk_dynamodb::Client) -> Self {
126        let table_name = TD::table_name();
127        let index_name = I::index_name();
128        tracing::debug!(table_name, index_name, "Scan (index)");
129        Self {
130            builder: client.scan().table_name(table_name).index_name(index_name),
131            _marker: PhantomData,
132        }
133    }
134
135    /// Enables strongly consistent reads for this scan.
136    ///
137    /// By default DynamoDB uses eventually consistent reads. Enabling consistent
138    /// reads guarantees the most up-to-date data but consumes twice the read
139    /// capacity units and is not supported on Global Secondary Indexes.
140    ///
141    /// # Examples
142    ///
143    /// ```no_run
144    /// # use dynamodb_facade::test_fixtures::*;
145    /// use dynamodb_facade::DynamoDBItemOp;
146    ///
147    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
148    /// let users /* : Vec<User> */ = User::scan(client).consistent_read().all().await?;
149    /// # Ok(())
150    /// # }
151    /// ```
152    pub fn consistent_read(mut self) -> Self {
153        tracing::debug!("Scan consistent_read");
154        self.builder = self.builder.consistent_read(true);
155        self
156    }
157
158    /// Sets the maximum number of items to evaluate per page.
159    ///
160    /// Note that DynamoDB evaluates up to `limit` items before applying any
161    /// filter expression, so the number of items returned may be less than
162    /// `limit` when a filter is active. Pagination continues automatically
163    /// when using [`.all()`][ScanRequest::all] or
164    /// [`.stream()`][ScanRequest::stream].
165    ///
166    /// # Examples
167    ///
168    /// ```no_run
169    /// # use dynamodb_facade::test_fixtures::*;
170    /// use dynamodb_facade::DynamoDBItemOp;
171    ///
172    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
173    /// let users /* : Vec<User> */ = User::scan(client).limit(100).all().await?;
174    /// # Ok(())
175    /// # }
176    /// ```
177    pub fn limit(mut self, limit: i32) -> Self {
178        tracing::debug!(limit, "Scan limit");
179        self.builder = self.builder.limit(limit);
180        self
181    }
182
183    /// Consumes the builder and returns the underlying SDK [`ScanFluentBuilder`].
184    ///
185    /// Use this escape hatch when you need to set options not exposed by this
186    /// facade, or when integrating with code that expects the raw SDK builder.
187    ///
188    /// # Examples
189    ///
190    /// ```no_run
191    /// # use dynamodb_facade::test_fixtures::*;
192    /// use dynamodb_facade::DynamoDBItemOp;
193    ///
194    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
195    /// let sdk_builder = User::scan(client).into_inner();
196    /// // configure sdk_builder further, then call .send().await
197    /// # Ok(())
198    /// # }
199    /// ```
200    pub fn into_inner(self) -> ScanFluentBuilder {
201        self.builder
202    }
203}
204
205// -- Filter (NoFilter only) -------------------------------------------------
206
207impl<TD: TableDefinition, T, O: OutputFormat, P: ProjectionState>
208    ScanRequest<TD, T, O, NoFilter, P>
209{
210    /// Adds a filter expression applied after items are read from the table.
211    ///
212    /// DynamoDB accepts a single filter expression per request, so this method
213    /// can only be called once. The filter is evaluated server-side after items
214    /// are read but before they are returned, so it does not reduce read
215    /// capacity consumption.
216    ///
217    /// # Examples
218    ///
219    /// ```no_run
220    /// # use dynamodb_facade::test_fixtures::*;
221    /// use dynamodb_facade::{DynamoDBItemOp, Condition};
222    ///
223    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
224    /// let instructors /* : Vec<User> */ = User::scan(client)
225    ///     .filter(Condition::eq("role", "instructor"))
226    ///     .all()
227    ///     .await?;
228    /// # Ok(())
229    /// # }
230    /// ```
231    pub fn filter(mut self, filter: Condition<'_>) -> ScanRequest<TD, T, O, AlreadyHasFilter, P> {
232        tracing::debug!(%filter, "Scan filter");
233        self.builder = filter.apply_filter(self.builder);
234        ScanRequest {
235            builder: self.builder,
236            _marker: PhantomData,
237        }
238    }
239}
240
241// -- Projection (NoProjection only) -----------------------------------------
242
243impl<TD: TableDefinition, T, O: OutputFormat, F: FilterState>
244    ScanRequest<TD, T, O, F, NoProjection>
245{
246    /// Applies a projection expression, limiting the attributes returned per item.
247    ///
248    /// This method can only be called once. It forces the output to raw
249    /// [`Item<TD>`] because projected results may not contain all fields
250    /// required for deserialization into `T`.
251    ///
252    /// # Examples
253    ///
254    /// ```no_run
255    /// # use dynamodb_facade::test_fixtures::*;
256    /// use dynamodb_facade::{DynamoDBItemOp, Projection};
257    ///
258    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
259    /// // Fetch only the "name" attribute for each user
260    /// let partial = User::scan(client)
261    ///     .project(Projection::new(["name"]))
262    ///     .all()
263    ///     .await?;
264    /// // partial: Vec<Item<PlatformTable>>
265    /// // with only "PK", "SK" and "name"
266    /// # Ok(())
267    /// # }
268    /// ```
269    pub fn project(
270        mut self,
271        projection: Projection<'_, TD>,
272    ) -> ScanRequest<TD, T, Raw, F, AlreadyHasProjection> {
273        tracing::debug!(%projection, "Scan project");
274        self.builder = projection.apply_projection(self.builder);
275        ScanRequest {
276            builder: self.builder,
277            _marker: PhantomData,
278        }
279    }
280}
281
282// -- Output format transition (preserve F, P) -------------------------------
283
284impl<TD: TableDefinition, T, F: FilterState, P: ProjectionState> ScanRequest<TD, T, Typed, F, P> {
285    /// Switches the output format from `Typed` to `Raw`.
286    ///
287    /// After calling `.raw()`, [`.all()`][ScanRequest::all] returns
288    /// `Vec<Item<TD>>` and [`.stream()`][ScanRequest::stream] yields
289    /// `Result<Vec<Item<TD>>>` (pages of raw items) instead of the typed
290    /// equivalents. This transition is one-way.
291    ///
292    /// # Examples
293    ///
294    /// ```no_run
295    /// # use dynamodb_facade::test_fixtures::*;
296    /// use dynamodb_facade::DynamoDBItemOp;
297    ///
298    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
299    /// let raw_items = User::scan(client).raw().all().await?;
300    /// // raw_items: Vec<Item<PlatformTable>>
301    /// # Ok(())
302    /// # }
303    /// ```
304    pub fn raw(self) -> ScanRequest<TD, T, Raw, F, P> {
305        ScanRequest {
306            builder: self.builder,
307            _marker: PhantomData,
308        }
309    }
310}
311
312// -- Terminal: Typed (any F, any P) -----------------------------------------
313
314impl<
315    TD: TableDefinition,
316    T: DynamoDBItem<TD> + DeserializeOwned,
317    F: FilterState,
318    P: ProjectionState,
319> ScanRequest<TD, T, Typed, F, P>
320{
321    /// Executes the scan, collecting all pages and returning items deserialized as `T`.
322    ///
323    /// Automatically follows pagination tokens until all matching items have
324    /// been retrieved. For large tables, prefer
325    /// [`.stream()`][ScanRequest::stream] to avoid loading everything into
326    /// memory at once.
327    ///
328    /// # Errors
329    ///
330    /// Returns [`Err`] if any DynamoDB page request fails or if deserialization
331    /// of any item fails.
332    ///
333    /// # Examples
334    ///
335    /// ```no_run
336    /// # use dynamodb_facade::test_fixtures::*;
337    /// use dynamodb_facade::DynamoDBItemOp;
338    ///
339    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
340    /// let all_users /* : Vec<User> */ = User::scan(client).all().await?;
341    /// # Ok(())
342    /// # }
343    /// ```
344    #[tracing::instrument(level = "debug", skip(self), name = "scan_all")]
345    pub async fn all(self) -> Result<Vec<T>> {
346        dynamodb_execute_scan(self.builder)
347            .await?
348            .into_iter()
349            .map(T::try_from_item)
350            .collect()
351    }
352
353    /// Executes the scan as a lazy async stream, yielding one page at a time.
354    ///
355    /// Each element yielded by the stream is a `Vec<T>` representing one page
356    /// of results deserialized as `T`. Pages are fetched on demand as the
357    /// stream is consumed. Use this for large tables where loading everything
358    /// into memory at once is undesirable.
359    ///
360    /// # Examples
361    ///
362    /// ```no_run
363    /// # use dynamodb_facade::test_fixtures::*;
364    /// use dynamodb_facade::DynamoDBItemOp;
365    /// use futures_util::StreamExt;
366    /// use std::pin::pin;
367    ///
368    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
369    /// let stream = User::scan(client).stream();
370    /// // Must pin the stream
371    /// let mut stream = pin!(stream);
372    ///
373    /// while let Some(result) = stream.next().await {
374    ///     let page /* : Vec<User> */ = result?;
375    ///     for user in page {
376    ///         let _ = user;
377    ///     }
378    /// }
379    /// # Ok(())
380    /// # }
381    /// ```
382    pub fn stream(self) -> impl Stream<Item = Result<Vec<T>>> {
383        dynamodb_stream_scan::<TD>(self.builder).map(|result| {
384            result.and_then(|items| items.into_iter().map(T::try_from_item).collect())
385        })
386    }
387}
388
389// -- Terminal: Raw (any F, any P) -------------------------------------------
390
391impl<TD: TableDefinition, T, F: FilterState, P: ProjectionState> ScanRequest<TD, T, Raw, F, P> {
392    /// Executes the scan, collecting all pages and returning raw item maps.
393    ///
394    /// Automatically follows pagination tokens until all items have been
395    /// retrieved.
396    ///
397    /// # Errors
398    ///
399    /// Returns [`Err`] if any DynamoDB page request fails.
400    ///
401    /// # Examples
402    ///
403    /// ```no_run
404    /// # use dynamodb_facade::test_fixtures::*;
405    /// use dynamodb_facade::ScanRequest;
406    ///
407    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
408    /// let items = ScanRequest::<PlatformTable>::new(client).all().await?;
409    /// // items: Vec<Item<PlatformTable>>
410    /// # Ok(())
411    /// # }
412    /// ```
413    #[tracing::instrument(level = "debug", skip(self), name = "scan_all_raw")]
414    pub async fn all(self) -> Result<Vec<Item<TD>>> {
415        dynamodb_execute_scan(self.builder).await
416    }
417
418    /// Executes the scan as a lazy async stream, yielding one page of raw item maps at a time.
419    ///
420    /// Each element yielded by the stream is a `Vec<Item<TD>>` representing one
421    /// page of results. Pages are fetched on demand as the stream is consumed.
422    ///
423    /// # Examples
424    ///
425    /// ```no_run
426    /// # use dynamodb_facade::test_fixtures::*;
427    /// use dynamodb_facade::ScanRequest;
428    /// use futures_util::StreamExt;
429    /// use std::pin::pin;
430    ///
431    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
432    /// let stream = ScanRequest::<PlatformTable>::new(client).stream();
433    /// // Must pin the stream
434    /// let mut stream = pin!(stream);
435    ///
436    /// while let Some(result) = stream.next().await {
437    ///     let page /* : Vec<Item<PlatformTable>> */ = result?;
438    ///     for item in page {
439    ///         let _ = item;
440    ///     }
441    /// }
442    /// # Ok(())
443    /// # }
444    /// ```
445    pub fn stream(self) -> impl Stream<Item = Result<Vec<Item<TD>>>> {
446        dynamodb_stream_scan(self.builder)
447    }
448}