Skip to main content

toasty_driver_integration_suite/tests/
deferred_field.rs

1use crate::prelude::*;
2
3#[driver_test(id(ID), scenario(crate::scenarios::deferred_document))]
4pub async fn default_load_leaves_deferred_unloaded(t: &mut Test) -> Result<()> {
5    let mut db = setup(t).await;
6
7    let created = toasty::create!(Document {
8        title: "Hello".to_string(),
9        body: "the long body".to_string(),
10    })
11    .exec(&mut db)
12    .await?;
13
14    // Newly created records expose the value just written as loaded.
15    assert_eq!("Hello", created.title);
16    assert_eq!("the long body", created.body.get());
17
18    // Querying the model leaves the deferred field unloaded.
19    let read = Document::filter_by_id(created.id).get(&mut db).await?;
20    assert_eq!("Hello", read.title);
21    assert!(read.body.is_unloaded());
22
23    Ok(())
24}
25
26#[driver_test(id(ID), scenario(crate::scenarios::deferred_document))]
27pub async fn deferred_include_loads_value(t: &mut Test) -> Result<()> {
28    let mut db = setup(t).await;
29
30    let created = toasty::create!(Document {
31        title: "Hello".to_string(),
32        body: "the long body".to_string(),
33    })
34    .exec(&mut db)
35    .await?;
36
37    // `.include()` of a deferred primitive eagerly loads it as part of the
38    // model query — no separate fetch is needed.
39    let read = Document::filter_by_id(created.id)
40        .include(Document::fields().body())
41        .get(&mut db)
42        .await?;
43
44    assert!(!read.body.is_unloaded());
45    assert_eq!("the long body", read.body.get());
46
47    Ok(())
48}
49
50#[driver_test(id(ID), scenario(crate::scenarios::deferred_optional_document))]
51pub async fn deferred_optional_include_loads_some(t: &mut Test) -> Result<()> {
52    let mut db = setup(t).await;
53
54    let created = toasty::create!(Document {
55        title: "With summary".to_string(),
56        summary: "a brief summary".to_string(),
57    })
58    .exec(&mut db)
59    .await?;
60
61    let read = Document::filter_by_id(created.id)
62        .include(Document::fields().summary())
63        .get(&mut db)
64        .await?;
65
66    assert!(!read.summary.is_unloaded());
67    assert_eq!(&Some("a brief summary".to_string()), read.summary.get());
68
69    Ok(())
70}
71
72#[driver_test(id(ID), scenario(crate::scenarios::deferred_optional_document))]
73pub async fn deferred_optional_include_loads_none(t: &mut Test) -> Result<()> {
74    // A nullable deferred field must distinguish "loaded as NULL" from
75    // "unloaded". An eager `.include()` puts the field into the loaded state
76    // even when the column value is NULL.
77    let mut db = setup(t).await;
78
79    let created = toasty::create!(Document {
80        title: "No summary".to_string(),
81    })
82    .exec(&mut db)
83    .await?;
84
85    let read = Document::filter_by_id(created.id)
86        .include(Document::fields().summary())
87        .get(&mut db)
88        .await?;
89
90    assert!(!read.summary.is_unloaded());
91    assert_eq!(&None, read.summary.get());
92
93    Ok(())
94}
95
96#[driver_test(id(ID), scenario(crate::scenarios::deferred_optional_document))]
97pub async fn deferred_optional_create_returns_none_loaded(t: &mut Test) -> Result<()> {
98    // INSERT...RETURNING bypasses the deferred mask, so the value the caller
99    // just supplied (including `None`) must come back loaded — the in-memory
100    // record should not be ambiguous with the unloaded state.
101    let mut db = setup(t).await;
102
103    let with_some = toasty::create!(Document {
104        title: "With summary".to_string(),
105        summary: "hello".to_string(),
106    })
107    .exec(&mut db)
108    .await?;
109
110    assert!(!with_some.summary.is_unloaded());
111    assert_eq!(&Some("hello".to_string()), with_some.summary.get());
112
113    let with_none = toasty::create!(Document {
114        title: "No summary".to_string(),
115    })
116    .exec(&mut db)
117    .await?;
118
119    assert!(!with_none.summary.is_unloaded());
120    assert_eq!(&None, with_none.summary.get());
121
122    Ok(())
123}
124
125#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::deferred_document))]
126pub async fn deferred_filter_does_not_load_field(t: &mut Test) -> Result<()> {
127    // SQL-only: a bare predicate on the deferred field requires a full table
128    // scan. The DDB equivalent is `deferred_pk_filter_does_not_load_field`,
129    // which grounds the query on the primary key.
130    let mut db = setup(t).await;
131
132    toasty::create!(Document {
133        title: "First".to_string(),
134        body: "alpha body".to_string(),
135    })
136    .exec(&mut db)
137    .await?;
138
139    toasty::create!(Document {
140        title: "Second".to_string(),
141        body: "beta body".to_string(),
142    })
143    .exec(&mut db)
144    .await?;
145
146    // Filter on the deferred field — the WHERE clause uses it but the SELECT
147    // does not project it.
148    let docs = Document::filter(Document::fields().body().eq("alpha body".to_string()))
149        .exec(&mut db)
150        .await?;
151
152    assert_eq!(1, docs.len());
153    assert_eq!("First", docs[0].title);
154    assert!(docs[0].body.is_unloaded());
155
156    Ok(())
157}
158
159#[driver_test(id(ID), scenario(crate::scenarios::deferred_document))]
160pub async fn deferred_pk_filter_does_not_load_field(t: &mut Test) -> Result<()> {
161    // Same coverage as `deferred_filter_does_not_load_field`, expressed as a
162    // PK-grounded query so it runs on DDB. The deferred field appears in the
163    // filter but is still left unloaded in the result.
164    let mut db = setup(t).await;
165
166    let alpha = toasty::create!(Document {
167        title: "First".to_string(),
168        body: "alpha body".to_string(),
169    })
170    .exec(&mut db)
171    .await?;
172
173    toasty::create!(Document {
174        title: "Second".to_string(),
175        body: "beta body".to_string(),
176    })
177    .exec(&mut db)
178    .await?;
179
180    // Match on the PK, with the deferred field as an additional filter.
181    let matched = Document::filter_by_id(alpha.id)
182        .filter(Document::fields().body().eq("alpha body".to_string()))
183        .exec(&mut db)
184        .await?;
185
186    assert_eq!(1, matched.len());
187    assert_eq!("First", matched[0].title);
188    assert!(matched[0].body.is_unloaded());
189
190    // The deferred predicate filters the row out when it does not match.
191    let missed = Document::filter_by_id(alpha.id)
192        .filter(Document::fields().body().eq("beta body".to_string()))
193        .exec(&mut db)
194        .await?;
195    assert!(missed.is_empty());
196
197    Ok(())
198}
199
200#[driver_test(id(ID))]
201pub async fn deferred_works_through_type_alias(t: &mut Test) -> Result<()> {
202    type Lazy<T> = toasty::Deferred<T>;
203
204    #[derive(Debug, toasty::Model)]
205    struct Document {
206        #[key]
207        #[auto]
208        id: ID,
209
210        title: String,
211        body: Lazy<String>,
212    }
213
214    let mut db = t.setup_db(models!(Document)).await;
215
216    let created = toasty::create!(Document {
217        title: "Hello".to_string(),
218        body: "the long body".to_string(),
219    })
220    .exec(&mut db)
221    .await?;
222
223    let read = Document::filter_by_id(created.id)
224        .include(Document::fields().body())
225        .get(&mut db)
226        .await?;
227    assert_eq!("the long body", read.body.get());
228
229    Ok(())
230}
231
232#[driver_test(id(ID), scenario(crate::scenarios::deferred_document))]
233pub async fn deferred_update_loads_from_unloaded(t: &mut Test) -> Result<()> {
234    // The caller supplied the value as part of the update, so the in-memory
235    // field becomes loaded — no follow-up fetch is needed.
236    let mut db = setup(t).await;
237
238    let created = toasty::create!(Document {
239        title: "Hello".to_string(),
240        body: "old body".to_string(),
241    })
242    .exec(&mut db)
243    .await?;
244
245    let mut doc = Document::filter_by_id(created.id).get(&mut db).await?;
246    assert!(doc.body.is_unloaded());
247
248    doc.update()
249        .body("new body".to_string())
250        .exec(&mut db)
251        .await?;
252
253    assert!(!doc.body.is_unloaded());
254    assert_eq!("new body", doc.body.get());
255
256    Ok(())
257}
258
259#[driver_test(id(ID), scenario(crate::scenarios::deferred_document))]
260pub async fn deferred_update_refreshes_loaded_value(t: &mut Test) -> Result<()> {
261    // An already-loaded deferred field is refreshed by the update, matching
262    // non-deferred field behavior.
263    let mut db = setup(t).await;
264
265    let created = toasty::create!(Document {
266        title: "Hello".to_string(),
267        body: "old body".to_string(),
268    })
269    .exec(&mut db)
270    .await?;
271
272    let mut doc = Document::filter_by_id(created.id)
273        .include(Document::fields().body())
274        .get(&mut db)
275        .await?;
276    assert_eq!("old body", doc.body.get());
277
278    doc.update()
279        .body("new body".to_string())
280        .exec(&mut db)
281        .await?;
282
283    assert!(!doc.body.is_unloaded());
284    assert_eq!("new body", doc.body.get());
285
286    Ok(())
287}
288
289// ---------- `Deferred<Json<T>>` on a single field ----------
290//
291// The column is stored as JSON, the in-memory field is `Deferred<Json<T>>`,
292// and `T` only implements `serde::{Serialize, Deserialize}` — never
293// Toasty's `Load` directly. Each behavior is exercised in isolation
294// against the shared scenario.
295
296#[driver_test(
297    id(ID),
298    requires(sql),
299    scenario(crate::scenarios::deferred_json_document)
300)]
301pub async fn deferred_json_create_returns_loaded(t: &mut Test) -> Result<()> {
302    let mut db = setup(t).await;
303
304    let initial = Payload {
305        name: "users".to_string(),
306        version: 1,
307    };
308
309    let created = toasty::create!(Repository {
310        name: "main".to_string(),
311        payload: initial.clone(),
312    })
313    .exec(&mut db)
314    .await?;
315
316    // INSERT...RETURNING echoes the value the caller supplied, so the field
317    // comes back already loaded — even though normal SELECTs would skip it.
318    assert!(!created.payload.is_unloaded());
319    assert_eq!(&initial, &created.payload.get().0);
320
321    Ok(())
322}
323
324#[driver_test(
325    id(ID),
326    requires(sql),
327    scenario(crate::scenarios::deferred_json_document)
328)]
329pub async fn deferred_json_default_load_leaves_unloaded(t: &mut Test) -> Result<()> {
330    let mut db = setup(t).await;
331
332    let created = toasty::create!(Repository {
333        name: "main".to_string(),
334        payload: Payload {
335            name: "users".to_string(),
336            version: 1,
337        },
338    })
339    .exec(&mut db)
340    .await?;
341
342    let read = Repository::filter_by_id(created.id).get(&mut db).await?;
343    assert!(read.payload.is_unloaded());
344
345    Ok(())
346}
347
348#[driver_test(
349    id(ID),
350    requires(sql),
351    scenario(crate::scenarios::deferred_json_document)
352)]
353pub async fn deferred_json_include_eager_loads_value(t: &mut Test) -> Result<()> {
354    let mut db = setup(t).await;
355
356    let initial = Payload {
357        name: "users".to_string(),
358        version: 1,
359    };
360
361    let created = toasty::create!(Repository {
362        name: "main".to_string(),
363        payload: initial.clone(),
364    })
365    .exec(&mut db)
366    .await?;
367
368    // `.include()` projects the JSON column into the SELECT; the model
369    // loader peels the deferred envelope, JSON-decodes the inner String,
370    // and wraps the resulting `Json<Payload>` back in a loaded `Deferred`.
371    let read = Repository::filter_by_id(created.id)
372        .include(Repository::fields().payload())
373        .get(&mut db)
374        .await?;
375    assert!(!read.payload.is_unloaded());
376    assert_eq!(&initial, &read.payload.get().0);
377
378    Ok(())
379}
380
381#[driver_test(
382    id(ID),
383    requires(sql),
384    scenario(crate::scenarios::deferred_json_document)
385)]
386pub async fn deferred_json_update_refreshes_loaded_value(t: &mut Test) -> Result<()> {
387    let mut db = setup(t).await;
388
389    let initial = Payload {
390        name: "users".to_string(),
391        version: 1,
392    };
393    let next = Payload {
394        name: "users".to_string(),
395        version: 2,
396    };
397
398    let created = toasty::create!(Repository {
399        name: "main".to_string(),
400        payload: initial,
401    })
402    .exec(&mut db)
403    .await?;
404
405    let mut doc = Repository::filter_by_id(created.id).get(&mut db).await?;
406    assert!(doc.payload.is_unloaded());
407
408    // The update echoes the assigned value back through the reload path,
409    // which JSON-decodes and re-wraps in `Deferred`.
410    doc.update().payload(next.clone()).exec(&mut db).await?;
411    assert!(!doc.payload.is_unloaded());
412    assert_eq!(&next, &doc.payload.get().0);
413
414    Ok(())
415}