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