Skip to main content

toasty_driver_integration_suite/tests/
relation_has_many_composite_key.rs

1//! Tests for `has_many` / `belongs_to` relationships whose foreign key spans
2//! multiple columns. These parallel the single-key relation tests so we can
3//! make sure composite-key behavior matches.
4//!
5//! Two scenarios are used:
6//!
7//! - [`crate::scenarios::composite_has_many_belongs_to`] — `User` with a
8//!   single-column auto PK and a `Todo` whose PK is
9//!   `#[key(partition = user_id, local = id)]`. The FK column is part of
10//!   the child's identity, so reassignment isn't supported on backends that
11//!   can't mutate primary-key columns (DynamoDB).
12//! - [`crate::scenarios::composite_fk_has_many_belongs_to`] — `User` with a
13//!   composite PK `(id, revision)`, and `Todo` with single PK and a
14//!   two-column indexed FK. Used by the reassignment tests, which need the
15//!   FK to be a plain updatable column.
16//!
17//! See: https://github.com/tokio-rs/toasty/discussions/904
18
19use crate::prelude::*;
20use hashbrown::HashMap;
21
22/// When a composite-key `belongs_to` has no covering index on the parent
23/// side, schema verification must return a helpful invalid-schema error
24/// rather than panicking with `failed to find relation index`.
25#[driver_test]
26pub async fn composite_belongs_to_missing_index_is_error(test: &mut Test) -> Result<()> {
27    #[derive(Debug, toasty::Model)]
28    #[key(id, revision)]
29    struct Parent {
30        id: String,
31        revision: i64,
32
33        #[has_many]
34        children: toasty::HasMany<Child>,
35    }
36
37    #[derive(Debug, toasty::Model)]
38    struct Child {
39        #[key]
40        id: String,
41
42        // Two field-level `#[index]` annotations create two separate
43        // single-column indexes — neither covers the composite foreign key.
44        #[index]
45        parent_id: String,
46        #[index]
47        parent_revision: i64,
48
49        #[belongs_to(key = [parent_id, parent_revision], references = [id, revision])]
50        parent: toasty::BelongsTo<Parent>,
51    }
52
53    let err = test
54        .try_setup_db(models!(Parent, Child))
55        .await
56        .expect_err("schema verification should reject this layout");
57
58    assert!(
59        err.is_invalid_schema(),
60        "expected invalid_schema error, got: {err}",
61    );
62
63    let msg = err.to_string();
64    assert!(
65        msg.contains("parent_id") && msg.contains("parent_revision"),
66        "error should mention the foreign-key fields, got: {msg}",
67    );
68    assert!(
69        msg.contains("#[index(parent_id, parent_revision)]"),
70        "error should suggest adding a composite index, got: {msg}",
71    );
72    // Both FK fields are individually `#[index]`-annotated, so the verifier
73    // should detect that and explain why two single-column indexes don't
74    // satisfy a composite foreign key.
75    assert!(
76        msg.contains("each foreign-key field already has its own `#[index]`"),
77        "error should call out the per-field `#[index]` annotations, got: {msg}",
78    );
79
80    Ok(())
81}
82
83// =====================================================================
84// Tier A — fundamental CRUD on a composite-FK has_many. These mirror the
85// single-key tests in `relation_has_many_crud.rs`.
86// =====================================================================
87
88#[driver_test(id(ID), scenario(crate::scenarios::composite_has_many_belongs_to))]
89pub async fn composite_crud_user_todos(test: &mut Test) -> Result<()> {
90    let mut db = setup(test).await;
91
92    let user = User::create().name("User 1").exec(&mut db).await?;
93
94    assert_eq!(0, user.todos().exec(&mut db).await?.len());
95
96    let todo = user
97        .todos()
98        .create()
99        .title("hello world")
100        .exec(&mut db)
101        .await?;
102
103    // Find the todo by its composite key
104    let list = Todo::filter_by_user_id_and_id(user.id, todo.id)
105        .exec(&mut db)
106        .await?;
107    assert_eq!(1, list.len());
108    assert_eq!(todo.id, list[0].id);
109
110    // Find by partition (user_id) only
111    let list = Todo::filter_by_user_id(user.id).exec(&mut db).await?;
112    assert_eq!(1, list.len());
113    assert_eq!(todo.id, list[0].id);
114
115    // Find the user via the todo's FK
116    let user_reload = User::get_by_id(&mut db, &todo.user_id).await?;
117    assert_eq!(user.id, user_reload.id);
118
119    let mut created = HashMap::new();
120    let mut ids = vec![todo.id];
121    created.insert(todo.id, todo);
122
123    for i in 0..5 {
124        let title = format!("hello world {i}");
125        let todo = if i.is_even() {
126            user.todos().create().title(title).exec(&mut db).await?
127        } else {
128            Todo::create()
129                .user(&user)
130                .title(title)
131                .exec(&mut db)
132                .await?
133        };
134        ids.push(todo.id);
135        assert_none!(created.insert(todo.id, todo));
136    }
137
138    let list = user.todos().exec(&mut db).await?;
139    assert_eq!(6, list.len());
140
141    let loaded: HashMap<_, _> = list.into_iter().map(|t| (t.id, t)).collect();
142    assert_eq!(6, loaded.len());
143    for (id, expect) in &created {
144        assert_eq!(expect.title, loaded[id].title);
145    }
146
147    let list = Todo::filter_by_user_id(user.id).exec(&mut db).await?;
148    assert_eq!(6, list.len());
149
150    let user2 = User::create().name("User 2").exec(&mut db).await?;
151    assert_eq!(0, user2.todos().exec(&mut db).await?.len());
152
153    let u2_todo = user2
154        .todos()
155        .create()
156        .title("user 2 todo")
157        .exec(&mut db)
158        .await?;
159
160    for todo in user.todos().exec(&mut db).await? {
161        assert_ne!(u2_todo.id, todo.id);
162    }
163
164    // Delete a TODO by value (lookup by composite PK)
165    let todo = Todo::get_by_user_id_and_id(&mut db, &user.id, &ids[0]).await?;
166    todo.delete().exec(&mut db).await?;
167    assert_err!(Todo::get_by_user_id_and_id(&mut db, &user.id, &ids[0]).await);
168    assert_err!(user.todos().get_by_id(&mut db, &ids[0]).await);
169
170    // Delete a TODO by scope
171    user.todos()
172        .filter_by_id(ids[1])
173        .delete()
174        .exec(&mut db)
175        .await?;
176    assert_err!(Todo::get_by_user_id_and_id(&mut db, &user.id, &ids[1]).await);
177    assert_err!(user.todos().get_by_id(&mut db, &ids[1]).await);
178
179    // Update a TODO via scope
180    user.todos()
181        .filter_by_id(ids[2])
182        .update()
183        .title("batch update 1")
184        .exec(&mut db)
185        .await?;
186    let todo = Todo::get_by_user_id_and_id(&mut db, &user.id, &ids[2]).await?;
187    assert_eq!(todo.title, "batch update 1");
188
189    // Updating via the wrong user's scope must NOT touch the todo
190    user2
191        .todos()
192        .filter_by_id(ids[2])
193        .update()
194        .title("batch update 2")
195        .exec(&mut db)
196        .await?;
197    let todo = Todo::get_by_user_id_and_id(&mut db, &user.id, &ids[2]).await?;
198    assert_eq!(todo.title, "batch update 1");
199
200    // Deleting the parent deletes its todos
201    let id = user.id;
202    user.delete().exec(&mut db).await?;
203    assert_err!(User::get_by_id(&mut db, &id).await);
204    assert_err!(Todo::get_by_user_id_and_id(&mut db, &id, &ids[2]).await);
205    Ok(())
206}
207
208#[driver_test(id(ID), scenario(crate::scenarios::composite_has_many_belongs_to))]
209pub async fn composite_has_many_insert_on_update(test: &mut Test) -> Result<()> {
210    let mut db = setup(test).await;
211
212    let mut user = User::create().name("Alice").exec(&mut db).await?;
213    assert!(user.todos().exec(&mut db).await?.is_empty());
214
215    user.update()
216        .name("Bob")
217        .todos(toasty::stmt::insert(Todo::create().title("change name")))
218        .exec(&mut db)
219        .await?;
220
221    assert_eq!("Bob", user.name);
222    let todos: Vec<_> = user.todos().exec(&mut db).await?;
223    assert_eq!(1, todos.len());
224    assert_eq!(todos[0].title, "change name");
225    Ok(())
226}
227
228#[driver_test(id(ID), scenario(crate::scenarios::composite_has_many_belongs_to))]
229pub async fn composite_scoped_find_by_id(test: &mut Test) -> Result<()> {
230    let mut db = setup(test).await;
231
232    let user1 = User::create().name("User 1").exec(&mut db).await?;
233    let user2 = User::create().name("User 2").exec(&mut db).await?;
234
235    let todo = user1
236        .todos()
237        .create()
238        .title("hello world")
239        .exec(&mut db)
240        .await?;
241
242    let reloaded = user1.todos().get_by_id(&mut db, &todo.id).await?;
243    assert_eq!(reloaded.id, todo.id);
244    assert_eq!(reloaded.title, todo.title);
245
246    // Other user's scope: must not find
247    assert_none!(
248        user2
249            .todos()
250            .filter_by_id(todo.id)
251            .first()
252            .exec(&mut db)
253            .await?
254    );
255
256    let reloaded = User::filter_by_id(user1.id)
257        .todos()
258        .get_by_id(&mut db, &todo.id)
259        .await?;
260    assert_eq!(reloaded.id, todo.id);
261
262    // Delete in the wrong scope is a no-op
263    user2
264        .todos()
265        .filter_by_id(todo.id)
266        .delete()
267        .exec(&mut db)
268        .await?;
269    let reloaded = user1.todos().get_by_id(&mut db, &todo.id).await?;
270    assert_eq!(reloaded.id, todo.id);
271    Ok(())
272}
273
274#[driver_test(id(ID), scenario(crate::scenarios::composite_has_many_belongs_to))]
275pub async fn composite_belongs_to_required(test: &mut Test) {
276    let mut db = setup(test).await;
277    assert_err!(Todo::create().title("orphan").exec(&mut db).await);
278}
279
280#[driver_test(id(ID))]
281pub async fn composite_delete_when_belongs_to_optional(test: &mut Test) -> Result<()> {
282    #[derive(Debug, toasty::Model)]
283    struct User {
284        #[key]
285        #[auto]
286        id: ID,
287
288        #[has_many]
289        todos: toasty::HasMany<Todo>,
290    }
291
292    // Composite FK where the FK is *optional* — modeled by leaving the
293    // partition-key column as `Option<ID>`. (NB: keys must still be
294    // populated when inserting, but the nullable FK exercises the
295    // belongs_to-optional codepath after the parent is deleted.)
296    #[derive(Debug, toasty::Model)]
297    struct Todo {
298        #[key]
299        #[auto]
300        id: uuid::Uuid,
301
302        #[index]
303        user_id: Option<ID>,
304
305        #[belongs_to(key = user_id, references = id)]
306        user: toasty::BelongsTo<Option<User>>,
307    }
308
309    let mut db = test.setup_db(models!(User, Todo)).await;
310
311    let user = User::create().exec(&mut db).await?;
312    let mut ids = vec![];
313
314    for _ in 0..3 {
315        let todo = user.todos().create().exec(&mut db).await?;
316        ids.push(todo.id);
317    }
318
319    user.delete().exec(&mut db).await?;
320
321    // Todos still exist; user_id is None
322    for id in ids {
323        let todo = Todo::get_by_id(&mut db, id).await?;
324        assert_none!(todo.user_id);
325    }
326    Ok(())
327}
328
329#[driver_test(scenario(crate::scenarios::composite_fk_has_many_belongs_to))]
330pub async fn composite_associate_new_user_with_todo_on_update_via_creation(
331    test: &mut Test,
332) -> Result<()> {
333    let mut db = setup(test).await;
334
335    let u1 = User::create()
336        .revision(1)
337        .name("User 1")
338        .todo(Todo::create().title("hello world"))
339        .exec(&mut db)
340        .await?;
341
342    let todos: Vec<_> = u1.todos().exec(&mut db).await?;
343    assert_eq!(1, todos.len());
344    let mut todo = todos.into_iter().next().unwrap();
345
346    todo.update()
347        .user(User::create().revision(1).name("User 2"))
348        .exec(&mut db)
349        .await?;
350    Ok(())
351}
352
353#[driver_test(scenario(crate::scenarios::composite_fk_has_many_belongs_to))]
354pub async fn composite_associate_new_user_with_todo_on_update_query_via_creation(
355    test: &mut Test,
356) -> Result<()> {
357    let mut db = setup(test).await;
358
359    let u1 = User::create()
360        .revision(1)
361        .name("User 1")
362        .todo(Todo::create().title("a todo"))
363        .exec(&mut db)
364        .await?;
365
366    let todos: Vec<_> = u1.todos().exec(&mut db).await?;
367    assert_eq!(1, todos.len());
368    let todo = todos.into_iter().next().unwrap();
369
370    Todo::filter_by_id(todo.id)
371        .update()
372        .user(User::create().revision(1).name("User 2"))
373        .exec(&mut db)
374        .await?;
375    Ok(())
376}
377
378#[driver_test(scenario(crate::scenarios::composite_fk_has_many_belongs_to))]
379pub async fn composite_assign_todo_that_already_has_user_on_create(test: &mut Test) -> Result<()> {
380    let mut db = setup(test).await;
381
382    let todo = Todo::create()
383        .title("a todo")
384        .user(User::create().revision(1).name("User 1"))
385        .exec(&mut db)
386        .await?;
387
388    let u1 = todo.user().exec(&mut db).await?;
389
390    let u2 = User::create()
391        .revision(1)
392        .name("User 2")
393        .todo(&todo)
394        .exec(&mut db)
395        .await?;
396
397    let todo_reload = Todo::get_by_id(&mut db, &todo.id).await?;
398    assert_eq!(u2.id, todo_reload.user_id);
399    assert_eq!(u2.revision, todo_reload.user_revision);
400
401    let todos: Vec<_> = u1.todos().exec(&mut db).await?;
402    assert_eq!(0, todos.len());
403
404    let todos: Vec<_> = u2.todos().exec(&mut db).await?;
405    assert_eq!(1, todos.len());
406    assert_eq!(todo.id, todos[0].id);
407    Ok(())
408}
409
410#[driver_test(scenario(crate::scenarios::composite_fk_has_many_belongs_to))]
411pub async fn composite_assign_todo_that_already_has_user_on_update(test: &mut Test) -> Result<()> {
412    let mut db = setup(test).await;
413
414    let todo = Todo::create()
415        .title("a todo")
416        .user(User::create().revision(1).name("User 1"))
417        .exec(&mut db)
418        .await?;
419
420    let u1 = todo.user().exec(&mut db).await?;
421
422    let mut u2 = User::create()
423        .revision(1)
424        .name("User 2")
425        .exec(&mut db)
426        .await?;
427
428    u2.update()
429        .todos(toasty::stmt::insert(&todo))
430        .exec(&mut db)
431        .await?;
432
433    let todo_reload = Todo::get_by_id(&mut db, &todo.id).await?;
434    assert_eq!(u2.id, todo_reload.user_id);
435    assert_eq!(u2.revision, todo_reload.user_revision);
436
437    let todos: Vec<_> = u1.todos().exec(&mut db).await?;
438    assert_eq!(0, todos.len());
439
440    let todos: Vec<_> = u2.todos().exec(&mut db).await?;
441    assert_eq!(1, todos.len());
442    assert_eq!(todo.id, todos[0].id);
443    Ok(())
444}
445
446#[driver_test(scenario(crate::scenarios::composite_fk_has_many_belongs_to))]
447pub async fn composite_assign_existing_user_to_todo(test: &mut Test) -> Result<()> {
448    let mut db = setup(test).await;
449
450    let mut todo = Todo::create()
451        .title("hello")
452        .user(User::create().revision(1).name("User 1"))
453        .exec(&mut db)
454        .await?;
455
456    let u1 = todo.user().exec(&mut db).await?;
457    let u2 = User::create()
458        .revision(1)
459        .name("User 2")
460        .exec(&mut db)
461        .await?;
462
463    todo.update().user(&u2).exec(&mut db).await?;
464
465    let todo_reload = Todo::get_by_id(&mut db, &todo.id).await?;
466    assert_eq!(u2.id, todo_reload.user_id);
467    assert_eq!(u2.revision, todo_reload.user_revision);
468
469    let todos: Vec<_> = u1.todos().exec(&mut db).await?;
470    assert_eq!(0, todos.len());
471
472    let todos: Vec<_> = u2.todos().exec(&mut db).await?;
473    assert_eq!(1, todos.len());
474    assert_eq!(todo.id, todos[0].id);
475    Ok(())
476}
477
478#[driver_test(id(ID), scenario(crate::scenarios::composite_has_many_belongs_to))]
479pub async fn composite_assign_todo_to_user_on_update_query(test: &mut Test) -> Result<()> {
480    let mut db = setup(test).await;
481
482    let user = User::create().name("User 1").exec(&mut db).await?;
483
484    User::filter_by_id(user.id)
485        .update()
486        .todos(toasty::stmt::insert(Todo::create().title("hello")))
487        .exec(&mut db)
488        .await?;
489
490    let todos: Vec<_> = user.todos().exec(&mut db).await?;
491    assert_eq!(1, todos.len());
492    assert_eq!("hello", todos[0].title);
493    Ok(())
494}
495
496// =====================================================================
497// Tier B — batch / link / unlink. Mirrors `relation_has_many_batch_create.rs`
498// and `relation_has_many_link_unlink.rs`.
499// =====================================================================
500
501#[driver_test(id(ID), scenario(crate::scenarios::composite_has_many_belongs_to))]
502pub async fn composite_user_batch_create_todos_one_level(test: &mut Test) -> Result<()> {
503    let mut db = setup(test).await;
504
505    let user = User::create()
506        .name("Ann Chovey")
507        .todo(Todo::create().title("Make pizza"))
508        .exec(&mut db)
509        .await?;
510
511    assert_eq!(user.name, "Ann Chovey");
512
513    let todos: Vec<_> = user.todos().exec(&mut db).await?;
514    assert_eq!(1, todos.len());
515    assert_eq!("Make pizza", todos[0].title);
516
517    let todo = Todo::get_by_user_id_and_id(&mut db, &user.id, &todos[0].id).await?;
518    assert_eq!("Make pizza", todo.title);
519    Ok(())
520}
521
522#[driver_test(id(ID), scenario(crate::scenarios::composite_has_many_belongs_to))]
523pub async fn composite_user_batch_create_two_todos_simple(test: &mut Test) -> Result<()> {
524    let mut db = setup(test).await;
525
526    let user = User::create()
527        .name("Ann Chovey")
528        .todo(Todo::create().title("Make pizza"))
529        .todo(Todo::create().title("Sleep"))
530        .exec(&mut db)
531        .await?;
532
533    let todos: Vec<_> = user.todos().exec(&mut db).await?;
534    assert_eq!(2, todos.len());
535
536    let mut titles: Vec<_> = todos.iter().map(|t| &t.title[..]).collect();
537    titles.sort();
538    assert_eq!(titles, vec!["Make pizza", "Sleep"]);
539    Ok(())
540}
541
542#[driver_test(id(ID))]
543pub async fn composite_user_batch_create_todos_with_optional_field(test: &mut Test) -> Result<()> {
544    #[derive(Debug, toasty::Model)]
545    struct User {
546        #[key]
547        #[auto]
548        id: ID,
549
550        name: String,
551
552        #[has_many]
553        todos: toasty::HasMany<Todo>,
554
555        // Optional field exercises the RETURNING/constantize path that
556        // regressed in #user_batch_create_todos_with_optional_field.
557        #[allow(dead_code)]
558        moto: Option<String>,
559    }
560
561    #[derive(Debug, toasty::Model)]
562    #[key(partition = user_id, local = id)]
563    struct Todo {
564        #[auto]
565        id: uuid::Uuid,
566
567        user_id: ID,
568
569        #[belongs_to(key = user_id, references = id)]
570        user: toasty::BelongsTo<User>,
571
572        title: String,
573    }
574
575    let mut db = test.setup_db(models!(User, Todo)).await;
576
577    let user = User::create()
578        .name("Ann Chovey")
579        .todo(Todo::create().title("Make pizza"))
580        .todo(Todo::create().title("Sleep"))
581        .exec(&mut db)
582        .await?;
583
584    let todos: Vec<_> = user.todos().exec(&mut db).await?;
585    assert_eq!(2, todos.len());
586
587    let mut titles: Vec<_> = todos.iter().map(|t| &t.title[..]).collect();
588    titles.sort();
589    assert_eq!(titles, vec!["Make pizza", "Sleep"]);
590    Ok(())
591}
592
593#[driver_test(id(ID))]
594pub async fn composite_remove_add_single_relation_option_belongs_to(test: &mut Test) -> Result<()> {
595    #[derive(Debug, toasty::Model)]
596    struct User {
597        #[key]
598        #[auto]
599        id: ID,
600
601        #[has_many]
602        todos: toasty::HasMany<Todo>,
603    }
604
605    #[derive(Debug, toasty::Model)]
606    struct Todo {
607        #[key]
608        #[auto]
609        id: uuid::Uuid,
610
611        #[index]
612        user_id: Option<ID>,
613
614        #[belongs_to(key = user_id, references = id)]
615        user: toasty::BelongsTo<Option<User>>,
616    }
617
618    let mut db = test.setup_db(models!(User, Todo)).await;
619
620    let user = User::create()
621        .todo(Todo::create())
622        .todo(Todo::create())
623        .exec(&mut db)
624        .await?;
625
626    let todos: Vec<_> = user.todos().exec(&mut db).await?;
627    assert_eq!(2, todos.len());
628
629    user.todos().remove(&mut db, &todos[0]).await?;
630
631    let todos_reloaded: Vec<_> = user.todos().exec(&mut db).await?;
632    assert_eq!(1, todos_reloaded.len());
633    assert_eq!(todos[1].id, todos_reloaded[0].id);
634
635    assert_err!(user.todos().get_by_id(&mut db, &todos[0].id).await);
636
637    let todo = Todo::get_by_id(&mut db, todos[0].id).await?;
638    assert_none!(todo.user_id);
639
640    user.todos().insert(&mut db, &todos[0]).await?;
641
642    let todos_reloaded: Vec<_> = user.todos().exec(&mut db).await?;
643    assert!(todos_reloaded.iter().any(|t| t.id == todos[0].id));
644    assert_ok!(user.todos().get_by_id(&mut db, todos[0].id).await);
645    Ok(())
646}
647
648#[driver_test(id(ID), scenario(crate::scenarios::composite_has_many_belongs_to))]
649pub async fn composite_add_remove_single_relation_required_belongs_to(
650    test: &mut Test,
651) -> Result<()> {
652    let mut db = setup(test).await;
653
654    let user = User::create().name("User 1").exec(&mut db).await?;
655
656    let t1 = user.todos().create().title("todo 1").exec(&mut db).await?;
657    let t2 = user.todos().create().title("todo 2").exec(&mut db).await?;
658    let t3 = user.todos().create().title("todo 3").exec(&mut db).await?;
659
660    let ids = vec![t1.id, t2.id, t3.id];
661
662    let todos_reloaded: Vec<_> = user.todos().exec(&mut db).await?;
663    assert_eq!(todos_reloaded.len(), 3);
664
665    for id in ids {
666        assert!(todos_reloaded.iter().any(|t| t.id == id));
667    }
668
669    // Unlinking a required belongs_to is a delete
670    user.todos().remove(&mut db, &todos_reloaded[0]).await?;
671
672    assert_err!(Todo::get_by_user_id_and_id(&mut db, &user.id, &todos_reloaded[0].id).await);
673
674    let todos_reloaded: Vec<_> = user.todos().exec(&mut db).await?;
675    assert_eq!(todos_reloaded.len(), 2);
676    Ok(())
677}
678
679#[driver_test(scenario(crate::scenarios::composite_fk_has_many_belongs_to))]
680pub async fn composite_reassign_relation_required_belongs_to(test: &mut Test) -> Result<()> {
681    let mut db = setup(test).await;
682
683    let u1 = User::create()
684        .revision(1)
685        .name("User 1")
686        .exec(&mut db)
687        .await?;
688    let u2 = User::create()
689        .revision(1)
690        .name("User 2")
691        .exec(&mut db)
692        .await?;
693
694    let t1 = u1.todos().create().title("a todo").exec(&mut db).await?;
695
696    u2.todos().insert(&mut db, &t1).await?;
697
698    assert!(u1.todos().exec(&mut db).await?.is_empty());
699
700    let todos = u2.todos().exec(&mut db).await?;
701    assert_eq!(1, todos.len());
702    assert_eq!(t1.id, todos[0].id);
703    Ok(())
704}
705
706#[driver_test(id(ID))]
707pub async fn composite_add_remove_multiple_relation_option_belongs_to(
708    test: &mut Test,
709) -> Result<()> {
710    #[derive(Debug, toasty::Model)]
711    struct User {
712        #[key]
713        #[auto]
714        id: ID,
715
716        #[has_many]
717        todos: toasty::HasMany<Todo>,
718    }
719
720    #[derive(Debug, toasty::Model)]
721    struct Todo {
722        #[key]
723        #[auto]
724        id: uuid::Uuid,
725
726        #[index]
727        user_id: Option<ID>,
728
729        #[belongs_to(key = user_id, references = id)]
730        user: toasty::BelongsTo<Option<User>>,
731    }
732
733    let mut db = test.setup_db(models!(User, Todo)).await;
734
735    let user = User::create().exec(&mut db).await?;
736
737    let t1 = Todo::create().exec(&mut db).await?;
738    let t2 = Todo::create().exec(&mut db).await?;
739    let t3 = Todo::create().exec(&mut db).await?;
740
741    let ids = vec![t1.id, t2.id, t3.id];
742
743    user.todos().insert(&mut db, &t1).await?;
744    user.todos().insert(&mut db, &t2).await?;
745    user.todos().insert(&mut db, &t3).await?;
746
747    let todos_reloaded: Vec<_> = user.todos().exec(&mut db).await?;
748    assert_eq!(todos_reloaded.len(), 3);
749
750    for id in ids {
751        assert!(todos_reloaded.iter().any(|t| t.id == id));
752    }
753    Ok(())
754}
755
756// =====================================================================
757// Tier C — preload (.include) over a composite FK. Mirrors
758// `relation_preload.rs`.
759// =====================================================================
760
761#[driver_test(id(ID), scenario(crate::scenarios::composite_has_many_belongs_to))]
762pub async fn composite_basic_has_many_and_belongs_to_preload(test: &mut Test) -> Result<()> {
763    let mut db = setup(test).await;
764
765    let user = User::create()
766        .name("Alice")
767        .todo(Todo::create().title("todo 1"))
768        .todo(Todo::create().title("todo 2"))
769        .todo(Todo::create().title("todo 3"))
770        .exec(&mut db)
771        .await?;
772
773    let user = User::filter_by_id(user.id)
774        .include(User::fields().todos())
775        .get(&mut db)
776        .await?;
777
778    assert_eq!(3, user.todos.get().len());
779
780    let id = user.todos.get()[0].id;
781    let user_id = user.todos.get()[0].user_id;
782
783    let todo = Todo::filter_by_user_id_and_id(user_id, id)
784        .include(Todo::fields().user())
785        .get(&mut db)
786        .await?;
787
788    assert_eq!(user.id, todo.user.get().id);
789    assert_eq!(user.id, todo.user_id);
790    Ok(())
791}
792
793#[driver_test(id(ID), scenario(crate::scenarios::composite_has_many_belongs_to))]
794pub async fn composite_preload_on_empty_query(test: &mut Test) -> Result<()> {
795    let mut db = setup(test).await;
796
797    User::create().name("Alice").exec(&mut db).await?;
798
799    let users: Vec<_> = User::filter(User::fields().name().eq("Nope"))
800        .include(User::fields().todos())
801        .exec(&mut db)
802        .await?;
803    assert!(users.is_empty());
804    Ok(())
805}
806
807#[driver_test(id(ID))]
808pub async fn composite_preload_has_many_with_optional_belongs_to(test: &mut Test) -> Result<()> {
809    #[derive(Debug, toasty::Model)]
810    struct User {
811        #[key]
812        #[auto]
813        id: ID,
814
815        name: String,
816
817        #[has_many]
818        todos: toasty::HasMany<Todo>,
819    }
820
821    #[derive(Debug, toasty::Model)]
822    struct Todo {
823        #[key]
824        #[auto]
825        id: uuid::Uuid,
826
827        #[index]
828        user_id: Option<ID>,
829
830        #[belongs_to(key = user_id, references = id)]
831        user: toasty::BelongsTo<Option<User>>,
832
833        title: String,
834    }
835
836    let mut db = test.setup_db(models!(User, Todo)).await;
837
838    let user = User::create()
839        .name("Alice")
840        .todo(Todo::create().title("alpha"))
841        .todo(Todo::create().title("beta"))
842        .exec(&mut db)
843        .await?;
844
845    let user = User::filter_by_id(user.id)
846        .include(User::fields().todos())
847        .get(&mut db)
848        .await?;
849
850    assert_eq!(2, user.todos.get().len());
851    Ok(())
852}
853
854#[driver_test(id(ID), scenario(crate::scenarios::composite_has_many_belongs_to))]
855pub async fn composite_nested_has_many_then_belongs_to_required(test: &mut Test) -> Result<()> {
856    let mut db = setup(test).await;
857
858    let user = User::create()
859        .name("Alice")
860        .todo(Todo::create().title("alpha"))
861        .todo(Todo::create().title("beta"))
862        .exec(&mut db)
863        .await?;
864
865    let user = User::filter_by_id(user.id)
866        .include(User::fields().todos().user())
867        .get(&mut db)
868        .await?;
869
870    let todos = user.todos.get();
871    assert_eq!(2, todos.len());
872    for todo in todos {
873        assert_eq!(user.id, todo.user.get().id);
874        assert_eq!("Alice", todo.user.get().name);
875    }
876    Ok(())
877}
878
879// =====================================================================
880// Tier D — has_one / belongs_to topologies with composite FK on the child.
881// =====================================================================
882
883#[driver_test(id(ID))]
884pub async fn composite_crud_has_one_required(test: &mut Test) -> Result<()> {
885    // User has a single auto-PK and a `has_one` Profile.
886    // Profile's PK is composite (`partition = user_id, local = id`), making the
887    // FK part of the row's key.
888    #[derive(Debug, toasty::Model)]
889    struct User {
890        #[key]
891        #[auto]
892        id: ID,
893
894        #[has_one]
895        profile: toasty::HasOne<Option<Profile>>,
896    }
897
898    #[derive(Debug, toasty::Model)]
899    #[key(partition = user_id, local = id)]
900    struct Profile {
901        #[auto]
902        id: uuid::Uuid,
903
904        user_id: ID,
905
906        #[belongs_to(key = user_id, references = id)]
907        user: toasty::BelongsTo<User>,
908
909        bio: String,
910    }
911
912    let mut db = test.setup_db(models!(User, Profile)).await;
913
914    let user = User::create()
915        .profile(Profile::create().bio("an apple a day"))
916        .exec(&mut db)
917        .await?;
918
919    let profile = user.profile().exec(&mut db).await?.unwrap();
920    assert_eq!(profile.bio, "an apple a day");
921
922    assert_eq!(user.id, profile.user().exec(&mut db).await?.id);
923
924    // Deleting the user also deletes the profile.
925    user.delete().exec(&mut db).await?;
926    assert_err!(Profile::get_by_user_id_and_id(&mut db, &profile.user_id, &profile.id).await);
927    Ok(())
928}
929
930// =====================================================================
931// Tier E — filters and projections through associations.
932// =====================================================================
933
934#[driver_test(id(ID), requires(sql))]
935pub async fn composite_filter_by_belongs_to_field(test: &mut Test) -> Result<()> {
936    #[derive(Debug, toasty::Model)]
937    struct User {
938        #[key]
939        #[auto]
940        id: ID,
941
942        name: String,
943    }
944
945    #[derive(Debug, toasty::Model)]
946    #[key(partition = user_id, local = id)]
947    struct Profile {
948        #[auto]
949        id: uuid::Uuid,
950
951        user_id: ID,
952
953        #[belongs_to(key = user_id, references = id)]
954        user: toasty::BelongsTo<User>,
955
956        bio: String,
957    }
958
959    let mut db = test.setup_db(models!(User, Profile)).await;
960
961    let alice = User::create().name("alice").exec(&mut db).await?;
962    let bob = User::create().name("bob").exec(&mut db).await?;
963
964    Profile::create()
965        .user(&alice)
966        .bio("alice's bio")
967        .exec(&mut db)
968        .await?;
969    Profile::create()
970        .user(&bob)
971        .bio("bob's bio")
972        .exec(&mut db)
973        .await?;
974
975    let profiles: Vec<Profile> = Profile::filter(Profile::fields().user().name().eq("alice"))
976        .exec(&mut db)
977        .await?;
978    assert_eq!(profiles.len(), 1);
979    assert_eq!(profiles[0].bio, "alice's bio");
980    Ok(())
981}
982
983#[driver_test(
984    id(ID),
985    requires(scan),
986    scenario(crate::scenarios::composite_has_many_belongs_to)
987)]
988pub async fn composite_filter_parent_by_child_field(test: &mut Test) -> Result<()> {
989    let mut db = setup(test).await;
990
991    let alice = User::create().name("Alice").exec(&mut db).await?;
992    let bob = User::create().name("Bob").exec(&mut db).await?;
993    let carol = User::create().name("Carol").exec(&mut db).await?;
994
995    alice.todos().create().title("urgent").exec(&mut db).await?;
996    bob.todos().create().title("later").exec(&mut db).await?;
997    carol.todos().create().title("urgent").exec(&mut db).await?;
998    carol.todos().create().title("later").exec(&mut db).await?;
999
1000    let users: Vec<_> = User::filter(
1001        User::fields()
1002            .todos()
1003            .any(Todo::fields().title().eq("urgent")),
1004    )
1005    .exec(&mut db)
1006    .await?;
1007
1008    assert_eq_unordered!(users.iter().map(|u| &u.name[..]), ["Alice", "Carol"]);
1009    Ok(())
1010}
1011
1012#[driver_test(id(ID), requires(scan))]
1013pub async fn composite_select_belongs_to_basic(test: &mut Test) -> Result<()> {
1014    #[derive(Debug, toasty::Model)]
1015    struct User {
1016        #[key]
1017        #[auto]
1018        id: ID,
1019        name: String,
1020    }
1021
1022    #[derive(Debug, toasty::Model)]
1023    #[key(partition = user_id, local = id)]
1024    struct Post {
1025        #[auto]
1026        id: uuid::Uuid,
1027        title: String,
1028
1029        user_id: ID,
1030
1031        #[belongs_to(key = user_id, references = id)]
1032        author: toasty::BelongsTo<User>,
1033    }
1034
1035    let mut db = test.setup_db(models!(User, Post)).await;
1036
1037    let alice = User::create().name("Alice").exec(&mut db).await?;
1038    Post::create()
1039        .title("Hello")
1040        .author(&alice)
1041        .exec(&mut db)
1042        .await?;
1043
1044    let users: Vec<User> = Post::all()
1045        .select(Post::fields().author())
1046        .exec(&mut db)
1047        .await?;
1048
1049    assert_eq!(users.len(), 1);
1050    assert_eq!(users[0].name, "Alice");
1051    Ok(())
1052}