Skip to main content

toasty_driver_integration_suite/tests/
deferred_embed.rs

1use crate::prelude::*;
2
3// ---------- Deferred<Embed> on a struct embed ----------
4
5#[driver_test(id(ID))]
6pub async fn deferred_embed_struct(t: &mut Test) -> Result<()> {
7    #[derive(Debug, toasty::Embed)]
8    struct Metadata {
9        author: String,
10        notes: String,
11    }
12
13    #[derive(Debug, toasty::Model)]
14    struct Document {
15        #[key]
16        #[auto]
17        id: ID,
18
19        title: String,
20
21        #[deferred]
22        metadata: toasty::Deferred<Metadata>,
23    }
24
25    let mut db = t.setup_db(models!(Document, Metadata)).await;
26
27    let created = toasty::create!(Document {
28        title: "Hello".to_string(),
29        metadata: Metadata {
30            author: "Alice".to_string(),
31            notes: "Important".to_string(),
32        },
33    })
34    .exec(&mut db)
35    .await?;
36
37    // Created records have the deferred embed loaded with the value the caller
38    // just supplied.
39    assert_eq!("Hello", created.title);
40    assert_eq!("Alice", created.metadata.get().author);
41    assert_eq!("Important", created.metadata.get().notes);
42
43    // A separate query leaves the deferred embed unloaded.
44    let read = Document::filter_by_id(created.id).get(&mut db).await?;
45    assert_eq!("Hello", read.title);
46    assert!(read.metadata.is_unloaded());
47
48    // The per-field accessor loads the embed by value.
49    let metadata: Metadata = read.metadata().exec(&mut db).await?;
50    assert_eq!("Alice", metadata.author);
51    assert_eq!("Important", metadata.notes);
52    // `.exec()` does not mutate the in-memory record.
53    assert!(read.metadata.is_unloaded());
54
55    // `.include()` preloads the embed onto the parent query.
56    let read_with = Document::filter_by_id(created.id)
57        .include(Document::fields().metadata())
58        .get(&mut db)
59        .await?;
60    assert!(!read_with.metadata.is_unloaded());
61    assert_eq!("Alice", read_with.metadata.get().author);
62    assert_eq!("Important", read_with.metadata.get().notes);
63
64    Ok(())
65}
66
67// `Deferred<Option<EmbeddedType>>` is not covered here. `Option<Embed>`
68// itself isn't yet supported as a model field — the schema layer has no
69// representation for a nullable embedded type, so the column-type lowering
70// errors out with "type Model(...) is not supported by this database".
71// Tracking that gap is orthogonal to deferred fields.
72
73// ---------- #[deferred] inside an embed struct that's nested in an enum variant ----------
74//
75// `#[deferred]` on a variant field directly is rejected at the macro layer,
76// but a struct embedded as a variant field is allowed to carry its own
77// deferred sub-fields. The lowering has to descend through the enum's
78// `Match` expression to mask / wrap those sub-fields.
79
80#[driver_test(id(ID))]
81pub async fn deferred_inside_embed_in_enum_variant(t: &mut Test) -> Result<()> {
82    #[derive(Debug, toasty::Embed)]
83    struct Metadata {
84        author: String,
85
86        #[deferred]
87        notes: toasty::Deferred<String>,
88    }
89
90    #[derive(Debug, toasty::Embed)]
91    enum ContactInfo {
92        Email { address: String, metadata: Metadata },
93        Phone { number: String },
94    }
95
96    #[derive(Debug, toasty::Model)]
97    struct Person {
98        #[key]
99        #[auto]
100        id: ID,
101
102        name: String,
103
104        contact: ContactInfo,
105    }
106
107    let mut db = t.setup_db(models!(Person, ContactInfo, Metadata)).await;
108
109    // INSERT...RETURNING must echo back the deferred sub-field nested two
110    // levels deep (through the enum variant and through the embed struct).
111    let created = toasty::create!(Person {
112        name: "Alice".to_string(),
113        contact: ContactInfo::Email {
114            address: "alice@example.com".to_string(),
115            metadata: Metadata {
116                author: "Alice".to_string(),
117                notes: "Important".to_string().into(),
118            },
119        },
120    })
121    .exec(&mut db)
122    .await?;
123
124    let ContactInfo::Email {
125        address, metadata, ..
126    } = &created.contact
127    else {
128        panic!("expected Email variant");
129    };
130    assert_eq!("alice@example.com", address);
131    assert_eq!("Alice", metadata.author);
132    assert_eq!("Important", metadata.notes.get());
133
134    // Default load: contact loaded, but the deferred sub-field nested inside
135    // the variant's Metadata embed is unloaded.
136    let read = Person::filter_by_id(created.id).get(&mut db).await?;
137    let ContactInfo::Email { metadata, .. } = &read.contact else {
138        panic!("expected Email variant");
139    };
140    assert_eq!("Alice", metadata.author);
141    assert!(metadata.notes.is_unloaded());
142
143    Ok(())
144}
145
146// `.include()` reaching a deferred sub-field that lives inside a struct embed
147// nested inside an enum variant. The variant handle exposes the same field
148// accessors as a struct embed, returning variant-rooted Paths that the
149// engine flattens into `[contact_idx, variant_idx, …]` projections and
150// dispatches into the matching arm of the embed enum's `Match`.
151
152#[driver_test(id(ID))]
153pub async fn include_deferred_inside_embed_in_enum_variant(t: &mut Test) -> Result<()> {
154    #[derive(Debug, toasty::Embed)]
155    struct Metadata {
156        author: String,
157
158        #[deferred]
159        notes: toasty::Deferred<String>,
160    }
161
162    #[derive(Debug, toasty::Embed)]
163    enum ContactInfo {
164        Email { address: String, metadata: Metadata },
165        Phone { number: String },
166    }
167
168    #[derive(Debug, toasty::Model)]
169    struct Person {
170        #[key]
171        #[auto]
172        id: ID,
173
174        name: String,
175
176        contact: ContactInfo,
177    }
178
179    let mut db = t.setup_db(models!(Person, ContactInfo, Metadata)).await;
180
181    let alice = toasty::create!(Person {
182        name: "Alice".to_string(),
183        contact: ContactInfo::Email {
184            address: "alice@example.com".to_string(),
185            metadata: Metadata {
186                author: "Alice".to_string(),
187                notes: "Important".to_string().into(),
188            },
189        },
190    })
191    .exec(&mut db)
192    .await?;
193
194    // Bob's contact is a different variant, used to verify that an include
195    // routed through Email doesn't activate anything for him.
196    let bob = toasty::create!(Person {
197        name: "Bob".to_string(),
198        contact: ContactInfo::Phone {
199            number: "555-0100".to_string(),
200        },
201    })
202    .exec(&mut db)
203    .await?;
204
205    // Alice's variant matches the path — `notes` arrives loaded.
206    let alice_read = Person::filter_by_id(alice.id)
207        .include(Person::fields().contact().email().metadata().notes())
208        .get(&mut db)
209        .await?;
210    let ContactInfo::Email { metadata, .. } = &alice_read.contact else {
211        panic!("expected Email variant");
212    };
213    assert_eq!("Alice", metadata.author);
214    assert!(!metadata.notes.is_unloaded());
215    assert_eq!("Important", metadata.notes.get());
216
217    // Bob's variant doesn't match the include path's arm — the include is a
218    // no-op for him: the row still loads cleanly with the Phone variant.
219    let bob_read = Person::filter_by_id(bob.id)
220        .include(Person::fields().contact().email().metadata().notes())
221        .get(&mut db)
222        .await?;
223    let ContactInfo::Phone { number } = &bob_read.contact else {
224        panic!("expected Phone variant");
225    };
226    assert_eq!("555-0100", number);
227
228    Ok(())
229}
230
231// ---------- Deferred<UnitEnum> ----------
232
233#[driver_test(id(ID))]
234pub async fn deferred_embed_unit_enum(t: &mut Test) -> Result<()> {
235    #[derive(Debug, PartialEq, toasty::Embed)]
236    enum Status {
237        Draft,
238        Published,
239        Archived,
240    }
241
242    #[derive(Debug, toasty::Model)]
243    struct Document {
244        #[key]
245        #[auto]
246        id: ID,
247
248        title: String,
249
250        #[deferred]
251        status: toasty::Deferred<Status>,
252    }
253
254    let mut db = t.setup_db(models!(Document, Status)).await;
255
256    let created = toasty::create!(Document {
257        title: "Hello".to_string(),
258        status: Status::Published,
259    })
260    .exec(&mut db)
261    .await?;
262
263    assert_eq!(&Status::Published, created.status.get());
264
265    let read = Document::filter_by_id(created.id).get(&mut db).await?;
266    assert!(read.status.is_unloaded());
267
268    let status: Status = read.status().exec(&mut db).await?;
269    assert_eq!(Status::Published, status);
270
271    let inc = Document::filter_by_id(created.id)
272        .include(Document::fields().status())
273        .get(&mut db)
274        .await?;
275    assert!(!inc.status.is_unloaded());
276    assert_eq!(&Status::Published, inc.status.get());
277
278    Ok(())
279}
280
281// ---------- Deferred<DataCarryingEnum> ----------
282
283#[driver_test(id(ID))]
284pub async fn deferred_embed_data_enum(t: &mut Test) -> Result<()> {
285    #[derive(Debug, PartialEq, toasty::Embed)]
286    enum ContactInfo {
287        Email { address: String },
288        Phone { number: String },
289        Mail,
290    }
291
292    #[derive(Debug, toasty::Model)]
293    struct Person {
294        #[key]
295        #[auto]
296        id: ID,
297
298        name: String,
299
300        #[deferred]
301        contact: toasty::Deferred<ContactInfo>,
302    }
303
304    let mut db = t.setup_db(models!(Person, ContactInfo)).await;
305
306    let alice = toasty::create!(Person {
307        name: "Alice".to_string(),
308        contact: ContactInfo::Email {
309            address: "alice@example.com".to_string(),
310        },
311    })
312    .exec(&mut db)
313    .await?;
314
315    let bob = toasty::create!(Person {
316        name: "Bob".to_string(),
317        contact: ContactInfo::Mail,
318    })
319    .exec(&mut db)
320    .await?;
321
322    assert_eq!(
323        &ContactInfo::Email {
324            address: "alice@example.com".to_string()
325        },
326        alice.contact.get()
327    );
328
329    let read = Person::filter_by_id(alice.id).get(&mut db).await?;
330    assert!(read.contact.is_unloaded());
331
332    let contact: ContactInfo = read.contact().exec(&mut db).await?;
333    assert_eq!(
334        ContactInfo::Email {
335            address: "alice@example.com".to_string()
336        },
337        contact
338    );
339
340    let read_bob = Person::filter_by_id(bob.id).get(&mut db).await?;
341    let contact: ContactInfo = read_bob.contact().exec(&mut db).await?;
342    assert_eq!(ContactInfo::Mail, contact);
343
344    let inc = Person::filter_by_id(alice.id)
345        .include(Person::fields().contact())
346        .get(&mut db)
347        .await?;
348    assert!(!inc.contact.is_unloaded());
349    assert_eq!(
350        &ContactInfo::Email {
351            address: "alice@example.com".to_string()
352        },
353        inc.contact.get()
354    );
355
356    Ok(())
357}
358
359// ---------- Updating a deferred embed reloads with the new value ----------
360
361#[driver_test(id(ID))]
362pub async fn deferred_embed_update_reloads(t: &mut Test) -> Result<()> {
363    #[derive(Debug, toasty::Embed)]
364    struct Metadata {
365        author: String,
366        notes: String,
367    }
368
369    #[derive(Debug, toasty::Model)]
370    struct Document {
371        #[key]
372        #[auto]
373        id: ID,
374
375        title: String,
376
377        #[deferred]
378        metadata: toasty::Deferred<Metadata>,
379    }
380
381    let mut db = t.setup_db(models!(Document, Metadata)).await;
382
383    let created = toasty::create!(Document {
384        title: "Hello".to_string(),
385        metadata: Metadata {
386            author: "Alice".to_string(),
387            notes: "old".to_string(),
388        },
389    })
390    .exec(&mut db)
391    .await?;
392
393    let mut doc = Document::filter_by_id(created.id).get(&mut db).await?;
394    assert!(doc.metadata.is_unloaded());
395
396    doc.update()
397        .metadata(Metadata {
398            author: "Bob".to_string(),
399            notes: "new".to_string(),
400        })
401        .exec(&mut db)
402        .await?;
403
404    // The caller supplied the value, so the field becomes loaded post-update.
405    assert!(!doc.metadata.is_unloaded());
406    assert_eq!("Bob", doc.metadata.get().author);
407    assert_eq!("new", doc.metadata.get().notes);
408
409    Ok(())
410}
411
412// ---------- Deferred<Embed> with #[deferred] inside the embed ----------
413//
414// The combined shape: the embed itself is deferred at the parent, AND the
415// embed has its own deferred sub-field. `.include(metadata())` loads the
416// outer wrapper but leaves the inner deferred sub-field unloaded;
417// `.include(metadata().notes())` loads both.
418
419#[driver_test(id(ID))]
420pub async fn deferred_embed_with_deferred_sub_field(t: &mut Test) -> Result<()> {
421    #[derive(Debug, toasty::Embed)]
422    struct Metadata {
423        author: String,
424
425        #[deferred]
426        notes: toasty::Deferred<String>,
427    }
428
429    #[derive(Debug, toasty::Model)]
430    struct Document {
431        #[key]
432        #[auto]
433        id: ID,
434
435        title: String,
436
437        #[deferred]
438        metadata: toasty::Deferred<Metadata>,
439    }
440
441    let mut db = t.setup_db(models!(Document, Metadata)).await;
442
443    let created = toasty::create!(Document {
444        title: "Hello".to_string(),
445        metadata: Metadata {
446            author: "Alice".to_string(),
447            notes: "Important".to_string().into(),
448        },
449    })
450    .exec(&mut db)
451    .await?;
452
453    // INSERT...RETURNING returns everything loaded.
454    assert_eq!("Alice", created.metadata.get().author);
455    assert_eq!("Important", created.metadata.get().notes.get());
456
457    // Default load: metadata itself unloaded.
458    let read = Document::filter_by_id(created.id).get(&mut db).await?;
459    assert!(read.metadata.is_unloaded());
460
461    // Including just the outer embed loads `author` but leaves the inner
462    // deferred sub-field unloaded.
463    let inc_outer = Document::filter_by_id(created.id)
464        .include(Document::fields().metadata())
465        .get(&mut db)
466        .await?;
467    assert!(!inc_outer.metadata.is_unloaded());
468    assert_eq!("Alice", inc_outer.metadata.get().author);
469    assert!(inc_outer.metadata.get().notes.is_unloaded());
470
471    // Including the inner deferred sub-field implies the outer is loaded too,
472    // and the inner sub-field arrives loaded.
473    let inc_inner = Document::filter_by_id(created.id)
474        .include(Document::fields().metadata().notes())
475        .get(&mut db)
476        .await?;
477    assert!(!inc_inner.metadata.is_unloaded());
478    assert_eq!("Alice", inc_inner.metadata.get().author);
479    assert!(!inc_inner.metadata.get().notes.is_unloaded());
480    assert_eq!("Important", inc_inner.metadata.get().notes.get());
481
482    Ok(())
483}
484
485// ---------- #[deferred] inside an Embed (per-column) ----------
486
487#[driver_test(id(ID))]
488pub async fn deferred_field_inside_embed(t: &mut Test) -> Result<()> {
489    #[derive(Debug, toasty::Embed)]
490    struct Metadata {
491        author: String,
492
493        #[deferred]
494        notes: toasty::Deferred<String>,
495    }
496
497    #[derive(Debug, toasty::Model)]
498    struct Document {
499        #[key]
500        #[auto]
501        id: ID,
502
503        title: String,
504
505        metadata: Metadata,
506    }
507
508    let mut db = t.setup_db(models!(Document, Metadata)).await;
509
510    let created = toasty::create!(Document {
511        title: "Hello".to_string(),
512        metadata: Metadata {
513            author: "Alice".to_string(),
514            notes: "Important".to_string().into(),
515        },
516    })
517    .exec(&mut db)
518    .await?;
519
520    // Created records carry the just-supplied value loaded.
521    assert_eq!("Alice", created.metadata.author);
522    assert_eq!("Important", created.metadata.notes.get());
523
524    // Default load: embed eager fields are loaded, deferred sub-field is not.
525    let read = Document::filter_by_id(created.id).get(&mut db).await?;
526    assert_eq!("Alice", read.metadata.author);
527    assert!(read.metadata.notes.is_unloaded());
528
529    // Including the deferred sub-field loads it on the same query.
530    let inc = Document::filter_by_id(created.id)
531        .include(Document::fields().metadata().notes())
532        .get(&mut db)
533        .await?;
534    assert!(!inc.metadata.notes.is_unloaded());
535    assert_eq!("Important", inc.metadata.notes.get());
536
537    Ok(())
538}
539
540// Updating an eager embed by whole-value when that embed contains a
541// `#[deferred]` sub-field. The struct literal supplies the deferred sub-field
542// via `From<T>` (`.into()`), the encoder unwraps it through
543// `Deferred<T>: IntoExpr<T>`, and the column is written.
544
545#[driver_test(id(ID))]
546pub async fn update_embed_by_value_with_deferred_sub_field(t: &mut Test) -> Result<()> {
547    #[derive(Debug, toasty::Embed)]
548    struct Metadata {
549        author: String,
550
551        #[deferred]
552        notes: toasty::Deferred<String>,
553    }
554
555    #[derive(Debug, toasty::Model)]
556    struct Document {
557        #[key]
558        #[auto]
559        id: ID,
560
561        title: String,
562
563        metadata: Metadata,
564    }
565
566    let mut db = t.setup_db(models!(Document, Metadata)).await;
567
568    let mut doc = toasty::create!(Document {
569        title: "Hello".to_string(),
570        metadata: Metadata {
571            author: "Alice".to_string(),
572            notes: "old".to_string().into(),
573        },
574    })
575    .exec(&mut db)
576    .await?;
577
578    doc.update()
579        .metadata(Metadata {
580            author: "Bob".to_string(),
581            notes: "new".to_string().into(),
582        })
583        .exec(&mut db)
584        .await?;
585
586    // Both columns are written and the in-memory record reflects the update.
587    assert_eq!("Bob", doc.metadata.author);
588    assert_eq!("new", doc.metadata.notes.get());
589
590    // Re-read with the deferred sub-field included to confirm both columns
591    // were persisted.
592    let reread = Document::filter_by_id(doc.id)
593        .include(Document::fields().metadata().notes())
594        .get(&mut db)
595        .await?;
596    assert_eq!("Bob", reread.metadata.author);
597    assert_eq!("new", reread.metadata.notes.get());
598
599    Ok(())
600}