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}