Skip to main content

toasty_driver_integration_suite/tests/
deferred_field.rs

1use crate::prelude::*;
2
3#[driver_test(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(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(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(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(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(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(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]
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: uuid::Uuid,
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(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(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(requires(sql), scenario(crate::scenarios::deferred_json_document))]
297pub async fn deferred_json_create_returns_loaded(t: &mut Test) -> Result<()> {
298    let mut db = setup(t).await;
299
300    let initial = Payload {
301        name: "users".to_string(),
302        version: 1,
303    };
304
305    let created = toasty::create!(Repository {
306        name: "main".to_string(),
307        payload: initial.clone(),
308    })
309    .exec(&mut db)
310    .await?;
311
312    // INSERT...RETURNING echoes the value the caller supplied, so the field
313    // comes back already loaded — even though normal SELECTs would skip it.
314    assert!(!created.payload.is_unloaded());
315    assert_eq!(&initial, &created.payload.get().0);
316
317    Ok(())
318}
319
320#[driver_test(requires(sql), scenario(crate::scenarios::deferred_json_document))]
321pub async fn deferred_json_default_load_leaves_unloaded(t: &mut Test) -> Result<()> {
322    let mut db = setup(t).await;
323
324    let created = toasty::create!(Repository {
325        name: "main".to_string(),
326        payload: Payload {
327            name: "users".to_string(),
328            version: 1,
329        },
330    })
331    .exec(&mut db)
332    .await?;
333
334    let read = Repository::filter_by_id(created.id).get(&mut db).await?;
335    assert!(read.payload.is_unloaded());
336
337    Ok(())
338}
339
340#[driver_test(requires(sql), scenario(crate::scenarios::deferred_json_document))]
341pub async fn deferred_json_include_eager_loads_value(t: &mut Test) -> Result<()> {
342    let mut db = setup(t).await;
343
344    let initial = Payload {
345        name: "users".to_string(),
346        version: 1,
347    };
348
349    let created = toasty::create!(Repository {
350        name: "main".to_string(),
351        payload: initial.clone(),
352    })
353    .exec(&mut db)
354    .await?;
355
356    // `.include()` projects the JSON column into the SELECT; the model
357    // loader peels the deferred envelope, JSON-decodes the inner String,
358    // and wraps the resulting `Json<Payload>` back in a loaded `Deferred`.
359    let read = Repository::filter_by_id(created.id)
360        .include(Repository::fields().payload())
361        .get(&mut db)
362        .await?;
363    assert!(!read.payload.is_unloaded());
364    assert_eq!(&initial, &read.payload.get().0);
365
366    Ok(())
367}
368
369#[driver_test(requires(sql), scenario(crate::scenarios::deferred_json_document))]
370pub async fn deferred_json_update_refreshes_loaded_value(t: &mut Test) -> Result<()> {
371    let mut db = setup(t).await;
372
373    let initial = Payload {
374        name: "users".to_string(),
375        version: 1,
376    };
377    let next = Payload {
378        name: "users".to_string(),
379        version: 2,
380    };
381
382    let created = toasty::create!(Repository {
383        name: "main".to_string(),
384        payload: initial,
385    })
386    .exec(&mut db)
387    .await?;
388
389    let mut doc = Repository::filter_by_id(created.id).get(&mut db).await?;
390    assert!(doc.payload.is_unloaded());
391
392    // The update echoes the assigned value back through the reload path,
393    // which JSON-decodes and re-wraps in `Deferred`.
394    doc.update().payload(next.clone()).exec(&mut db).await?;
395    assert!(!doc.payload.is_unloaded());
396    assert_eq!(&next, &doc.payload.get().0);
397
398    Ok(())
399}