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