Skip to main content

toasty_driver_integration_suite/tests/
embed_relation.rs

1use crate::prelude::*;
2
3/// The polymorphic-owner shape: `#[belongs_to]` fields inside embedded enum
4/// variants, exercised through the full CRUD cycle. The relation fields map
5/// to no columns — the discriminant and the key fields own the storage.
6/// Creating supplies the variant value with explicit keys, `match` reads the
7/// stored keys back, the owner loads with an ordinary `get_by_*`, and
8/// changing the owner — including its kind — is a whole-value replacement of
9/// the embed.
10#[driver_test]
11pub async fn belongs_to_in_enum_variants(test: &mut Test) -> Result<()> {
12    #[derive(Debug, toasty::Model)]
13    struct Human {
14        #[key]
15        #[auto]
16        id: uuid::Uuid,
17        name: String,
18    }
19
20    #[derive(Debug, toasty::Model)]
21    struct Bot {
22        #[key]
23        #[auto]
24        id: uuid::Uuid,
25        #[unique]
26        serial: String,
27        name: String,
28    }
29
30    #[derive(Debug, toasty::Embed)]
31    enum Owner {
32        Human {
33            #[index]
34            id: uuid::Uuid,
35            #[belongs_to(key = id)]
36            human: toasty::Deferred<Human>,
37        },
38        Bot {
39            #[index]
40            serial: String,
41            #[belongs_to(key = serial, references = serial)]
42            bot: toasty::Deferred<Bot>,
43        },
44    }
45
46    #[derive(Debug, toasty::Model)]
47    struct Object {
48        #[key]
49        #[auto]
50        id: uuid::Uuid,
51        owner: Owner,
52    }
53
54    let mut db = test.setup_db(models!(Object, Human, Bot)).await;
55
56    // The relation fields contribute no columns: discriminant + one column
57    // per key field.
58    let table = &db.schema().db.tables[0];
59    let names: Vec<_> = table.columns.iter().map(|c| c.name.as_str()).collect();
60    assert_eq!(names, ["id", "owner", "owner_id", "owner_serial"]);
61
62    let alice = toasty::create!(Human { name: "Alice" })
63        .exec(&mut db)
64        .await?;
65    let bot = toasty::create!(Bot {
66        serial: "B-1000",
67        name: "Marvin"
68    })
69    .exec(&mut db)
70    .await?;
71
72    // Create with explicit keys; the relation stays unloaded.
73    let obj_a = toasty::create!(Object {
74        owner: Owner::Human {
75            id: alice.id,
76            human: toasty::Deferred::default(),
77        }
78    })
79    .exec(&mut db)
80    .await?;
81    let obj_b = toasty::create!(Object {
82        owner: Owner::Bot {
83            serial: bot.serial.clone(),
84            bot: toasty::Deferred::default(),
85        }
86    })
87    .exec(&mut db)
88    .await?;
89
90    // `match` gives direct access to the stored keys; the owner loads with an
91    // ordinary lookup.
92    let mut obj_a = Object::get_by_id(&mut db, obj_a.id).await?;
93    match &obj_a.owner {
94        Owner::Human { id, human } => {
95            assert!(human.is_unloaded());
96            let human = Human::get_by_id(&mut db, id).await?;
97            assert_eq!(human.name, "Alice");
98        }
99        other => panic!("expected Owner::Human, got {other:?}"),
100    }
101
102    let obj_b = Object::get_by_id(&mut db, obj_b.id).await?;
103    match &obj_b.owner {
104        Owner::Bot { serial, bot } => {
105            assert!(bot.is_unloaded());
106            let bot = Bot::get_by_serial(&mut db, serial).await?;
107            assert_eq!(bot.name, "Marvin");
108        }
109        other => panic!("expected Owner::Bot, got {other:?}"),
110    }
111
112    // Changing the owner — including its kind — is a whole-value replacement
113    // of the embed.
114    obj_a
115        .update()
116        .owner(Owner::Bot {
117            serial: bot.serial.clone(),
118            bot: toasty::Deferred::default(),
119        })
120        .exec(&mut db)
121        .await?;
122    let reloaded = Object::get_by_id(&mut db, obj_a.id).await?;
123    assert_struct!(
124        reloaded.owner,
125        Owner::Bot {
126            serial: "B-1000",
127            ..
128        }
129    );
130
131    let obj_a_id = obj_a.id;
132    obj_a.delete().exec(&mut db).await?;
133    assert_err!(Object::get_by_id(&mut db, obj_a_id).await);
134    assert!(matches!(
135        Object::get_by_id(&mut db, obj_b.id).await?.owner,
136        Owner::Bot { .. }
137    ));
138
139    Ok(())
140}
141
142/// Key fields of relation-carrying variants stay queryable through the
143/// existing variant filter paths — the variant closure gates on the
144/// discriminant and compares the shared key column — and stay consistent
145/// as rows are re-pointed and deleted.
146#[driver_test]
147pub async fn filter_by_relation_key_through_variant_path(test: &mut Test) -> Result<()> {
148    #[derive(Debug, toasty::Model)]
149    struct Human {
150        #[key]
151        #[auto]
152        id: uuid::Uuid,
153        name: String,
154    }
155
156    #[derive(Debug, toasty::Model)]
157    struct Animal {
158        #[key]
159        #[auto]
160        id: uuid::Uuid,
161        name: String,
162    }
163
164    // `Human` and `Animal` share one key column; `#[index(id)]` indexes it
165    // once for both.
166    #[derive(Debug, toasty::Embed)]
167    #[index(id)]
168    enum Owner {
169        Human {
170            #[shared(id)]
171            id: uuid::Uuid,
172            #[belongs_to(key = id)]
173            human: toasty::Deferred<Human>,
174        },
175        Animal {
176            #[shared(id)]
177            id: uuid::Uuid,
178            #[belongs_to(key = id)]
179            animal: toasty::Deferred<Animal>,
180        },
181    }
182
183    #[derive(Debug, toasty::Model)]
184    struct Object {
185        #[key]
186        #[auto]
187        id: uuid::Uuid,
188        owner: Owner,
189    }
190
191    let mut db = test.setup_db(models!(Object, Human, Animal)).await;
192
193    // One shared key column, indexed by the enum-level attribute.
194    let table = &db.schema().db.tables[0];
195    let names: Vec<_> = table.columns.iter().map(|c| c.name.as_str()).collect();
196    assert_eq!(names, ["id", "owner", "owner_id"]);
197    let key_col = columns(&db, "objects", &["owner_id"])[0];
198    assert_struct!(table.indices, [
199        { primary_key: true },
200        { unique: false, primary_key: false, columns: [{ column: == key_col }] },
201    ]);
202
203    let alice = toasty::create!(Human { name: "Alice" })
204        .exec(&mut db)
205        .await?;
206    // An animal holding the same UUID as Alice, to prove the variant gate.
207    let rex = toasty::create!(Animal { name: "Rex" })
208        .exec(&mut db)
209        .await?;
210
211    let mut human_obj = toasty::create!(Object {
212        owner: Owner::Human {
213            id: alice.id,
214            human: toasty::Deferred::default(),
215        }
216    })
217    .exec(&mut db)
218    .await?;
219    toasty::create!(Object {
220        owner: Owner::Animal {
221            id: rex.id,
222            animal: toasty::Deferred::default(),
223        }
224    })
225    .exec(&mut db)
226    .await?;
227
228    // Variant-gated key filter: only the Human row matches, even though the
229    // Animal row stores its key in the same column.
230    let by_key = |id: uuid::Uuid| {
231        Object::fields()
232            .owner()
233            .human()
234            .matches(move |h| h.id().eq(id))
235    };
236    let found: Vec<Object> = Object::filter(by_key(alice.id)).exec(&mut db).await?;
237    assert_eq!(found.len(), 1);
238    assert_eq!(found[0].id, human_obj.id);
239
240    // The discriminant filter alone works as before.
241    let humans: Vec<Object> = Object::filter(Object::fields().owner().is_human())
242        .exec(&mut db)
243        .await?;
244    assert_eq!(humans.len(), 1);
245    assert_eq!(humans[0].id, human_obj.id);
246
247    // Re-point the relation within the same variant by replacing the embed
248    // value; the key filter follows.
249    let bea = toasty::create!(Human { name: "Bea" }).exec(&mut db).await?;
250    human_obj
251        .update()
252        .owner(Owner::Human {
253            id: bea.id,
254            human: toasty::Deferred::default(),
255        })
256        .exec(&mut db)
257        .await?;
258    assert!(
259        Object::filter(by_key(alice.id))
260            .exec(&mut db)
261            .await?
262            .is_empty()
263    );
264    let found: Vec<Object> = Object::filter(by_key(bea.id)).exec(&mut db).await?;
265    assert_eq!(found.len(), 1);
266    assert_eq!(found[0].id, human_obj.id);
267
268    // Deleting the row empties the key filter; the Animal row is untouched.
269    human_obj.delete().exec(&mut db).await?;
270    assert!(
271        Object::filter(by_key(bea.id))
272            .exec(&mut db)
273            .await?
274            .is_empty()
275    );
276    let animals: Vec<Object> = Object::filter(Object::fields().owner().is_animal())
277        .exec(&mut db)
278        .await?;
279    assert_eq!(animals.len(), 1);
280
281    Ok(())
282}
283
284/// `#[belongs_to]` inside an embedded struct: same storage rule — the key
285/// field owns the column, the relation maps to nothing — through the full
286/// CRUD cycle.
287#[driver_test]
288pub async fn belongs_to_in_embedded_struct(test: &mut Test) -> Result<()> {
289    #[derive(Debug, toasty::Model)]
290    struct Author {
291        #[key]
292        #[auto]
293        id: uuid::Uuid,
294        name: String,
295    }
296
297    #[derive(Debug, toasty::Embed)]
298    struct Attribution {
299        #[index]
300        author_id: uuid::Uuid,
301        #[belongs_to(key = author_id)]
302        author: toasty::Deferred<Author>,
303        note: String,
304    }
305
306    #[derive(Debug, toasty::Model)]
307    struct Post {
308        #[key]
309        #[auto]
310        id: uuid::Uuid,
311        attribution: Attribution,
312    }
313
314    let mut db = test.setup_db(models!(Post, Author)).await;
315
316    let table = &db.schema().db.tables[0];
317    let names: Vec<_> = table.columns.iter().map(|c| c.name.as_str()).collect();
318    assert_eq!(names, ["id", "attribution_author_id", "attribution_note"]);
319
320    let author = toasty::create!(Author { name: "Ann" })
321        .exec(&mut db)
322        .await?;
323
324    let post = toasty::create!(Post {
325        attribution: Attribution {
326            author_id: author.id,
327            author: toasty::Deferred::default(),
328            note: "first draft".to_string(),
329        }
330    })
331    .exec(&mut db)
332    .await?;
333
334    let mut post = Post::get_by_id(&mut db, post.id).await?;
335    assert!(post.attribution.author.is_unloaded());
336    assert_eq!(post.attribution.note, "first draft");
337    let author = Author::get_by_id(&mut db, &post.attribution.author_id).await?;
338    assert_eq!(author.name, "Ann");
339
340    // The key field stays queryable through the embed path.
341    let found: Vec<Post> = Post::filter(
342        Post::fields()
343            .attribution()
344            .author_id()
345            .eq(post.attribution.author_id),
346    )
347    .exec(&mut db)
348    .await?;
349    assert_eq!(found.len(), 1);
350
351    // And assignable through the embed update builder.
352    let other = toasty::create!(Author { name: "Bea" })
353        .exec(&mut db)
354        .await?;
355    post.update()
356        .attribution(toasty::stmt::patch(
357            Attribution::fields().author_id(),
358            other.id,
359        ))
360        .exec(&mut db)
361        .await?;
362    assert_eq!(post.attribution.author_id, other.id);
363    assert_eq!(
364        Post::get_by_id(&mut db, post.id)
365            .await?
366            .attribution
367            .author_id,
368        other.id
369    );
370
371    let post_id = post.id;
372    post.delete().exec(&mut db).await?;
373    assert_err!(Post::get_by_id(&mut db, post_id).await);
374
375    Ok(())
376}
377
378/// An `Option<Owner>` field: an ownerless row stores NULL in the discriminant
379/// column, per existing optional-embed support, and updates move rows in and
380/// out of ownership.
381#[driver_test]
382pub async fn optional_relation_carrying_embed(test: &mut Test) -> Result<()> {
383    #[derive(Debug, toasty::Model)]
384    struct Human {
385        #[key]
386        #[auto]
387        id: uuid::Uuid,
388    }
389
390    #[derive(Debug, toasty::Embed)]
391    enum Owner {
392        Human {
393            #[index]
394            id: uuid::Uuid,
395            #[belongs_to(key = id)]
396            human: toasty::Deferred<Human>,
397        },
398    }
399
400    #[derive(Debug, toasty::Model)]
401    struct Object {
402        #[key]
403        #[auto]
404        id: uuid::Uuid,
405        owner: Option<Owner>,
406    }
407
408    let mut db = test.setup_db(models!(Object, Human)).await;
409
410    let orphan = toasty::create!(Object { owner: None })
411        .exec(&mut db)
412        .await?;
413
414    let human = toasty::create!(Human {}).exec(&mut db).await?;
415    let owned = toasty::create!(Object {
416        owner: Some(Owner::Human {
417            id: human.id,
418            human: toasty::Deferred::default(),
419        })
420    })
421    .exec(&mut db)
422    .await?;
423
424    let mut orphan = Object::get_by_id(&mut db, orphan.id).await?;
425    assert!(orphan.owner.is_none());
426
427    let mut owned = Object::get_by_id(&mut db, owned.id).await?;
428    match &owned.owner {
429        Some(Owner::Human { id, human: rel }) => {
430            assert_eq!(*id, human.id);
431            assert!(rel.is_unloaded());
432        }
433        other => panic!("expected Some(Owner::Human), got {other:?}"),
434    }
435
436    // Assign an owner to the ownerless row and clear the owned row.
437    orphan
438        .update()
439        .owner(Some(Owner::Human {
440            id: human.id,
441            human: toasty::Deferred::default(),
442        }))
443        .exec(&mut db)
444        .await?;
445    assert!(matches!(
446        Object::get_by_id(&mut db, orphan.id).await?.owner,
447        Some(Owner::Human { .. })
448    ));
449
450    owned.update().owner(None).exec(&mut db).await?;
451    assert!(Object::get_by_id(&mut db, owned.id).await?.owner.is_none());
452
453    Ok(())
454}