toasty_driver_integration_suite/tests/
embedded_struct.rs

1use toasty::schema::{
2    app::FieldTy,
3    mapping::{self, FieldPrimitive, FieldStruct},
4};
5use toasty_core::stmt;
6use uuid::Uuid;
7
8use crate::prelude::*;
9
10/// Tests that embedded structs are registered in the app schema but don't create
11/// their own database tables (they're inlined into parent models).
12#[driver_test]
13pub async fn basic_embedded_struct(test: &mut Test) {
14    #[derive(toasty::Embed)]
15    struct Address {
16        street: String,
17        city: String,
18    }
19
20    let db = test.setup_db(models!(Address)).await;
21    let schema = db.schema();
22
23    // Embedded models exist in app schema as Model::EmbeddedStruct
24    assert_struct!(schema.app.models, #{
25        Address::id(): toasty::schema::app::Model::EmbeddedStruct(_ {
26            name.upper_camel_case(): "Address",
27            fields: [
28                _ { name.app_name: "street", .. },
29                _ { name.app_name: "city", .. },
30            ],
31            ..
32        }),
33    });
34
35    // Embedded models don't create database tables (fields are flattened into parent)
36    assert!(schema.db.tables.is_empty());
37}
38
39/// Tests the complete schema generation and mapping for embedded fields:
40/// - App schema: embedded field with correct type reference
41/// - DB schema: embedded fields flattened to columns (address_street, address_city)
42/// - Mapping: projection expressions for field lowering/lifting
43#[driver_test]
44pub async fn root_model_with_embedded_field(test: &mut Test) {
45    #[derive(toasty::Embed)]
46    struct Address {
47        street: String,
48        city: String,
49    }
50
51    #[derive(toasty::Model)]
52    struct User {
53        #[key]
54        id: String,
55        #[allow(dead_code)]
56        address: Address,
57    }
58
59    let db = test.setup_db(models!(User, Address)).await;
60    let schema = db.schema();
61
62    // Both embedded and root models exist in app schema
63    assert_struct!(schema.app.models, #{
64        Address::id(): toasty::schema::app::Model::EmbeddedStruct(_ {
65            name.upper_camel_case(): "Address",
66            fields: [
67                _ { name.app_name: "street", .. },
68                _ { name.app_name: "city", .. },
69            ],
70            ..
71        }),
72        User::id(): toasty::schema::app::Model::Root(_ {
73            name.upper_camel_case(): "User",
74            fields: [
75                _ { name.app_name: "id", .. },
76                _ {
77                    name.app_name: "address",
78                    ty: FieldTy::Embedded(_ {
79                        target: == Address::id(),
80                        ..
81                    }),
82                    ..
83                },
84            ],
85            ..
86        }),
87    });
88
89    // Database table has flattened columns with prefix (address_street, address_city)
90    // This is the key transformation: embedded struct fields become individual columns
91    assert_struct!(schema.db.tables, [
92        _ {
93            name: =~ r"users$",
94            columns: [
95                _ { name: "id", .. },
96                _ { name: "address_street", .. },
97                _ { name: "address_city", .. },
98            ],
99            ..
100        },
101    ]);
102
103    let user = &schema.app.models[&User::id()];
104    let user_table = schema.table_for(user);
105    let user_mapping = &schema.mapping.models[&User::id()];
106
107    // Mapping contains projection expressions that extract embedded fields
108    // Model -> Table (lowering): project(address_field, [0]) extracts street
109    // This allows queries like User.address.city to become address_city column refs
110    assert_struct!(user_mapping, _ {
111        columns.len(): 3,
112        fields: [
113            mapping::Field::Primitive(FieldPrimitive {
114                column: == user_table.columns[0].id,
115                lowering: 0,
116                ..
117            }),
118            mapping::Field::Struct(FieldStruct {
119                fields: [
120                    mapping::Field::Primitive(FieldPrimitive {
121                        column: == user_table.columns[1].id,
122                        lowering: 1,
123                        ..
124                    }),
125                    mapping::Field::Primitive(FieldPrimitive {
126                        column: == user_table.columns[2].id,
127                        lowering: 2,
128                        ..
129                    }),
130                ],
131                ..
132            }),
133        ],
134        model_to_table.fields: [
135            _,
136            == stmt::Expr::project(
137                stmt::Expr::ref_self_field(user.expect_root().fields[1].id),
138                [0],
139            ),
140            == stmt::Expr::project(
141                stmt::Expr::ref_self_field(user.expect_root().fields[1].id),
142                [1],
143            ),
144        ],
145        ..
146    });
147
148    // Table -> Model (lifting): columns are grouped back into record
149    // [id_col, street_col, city_col] -> [id, record([street_col, city_col])]
150    let table_to_model = user_mapping
151        .table_to_model
152        .lower_returning_model()
153        .into_record();
154
155    assert_struct!(
156        table_to_model.fields,
157        [
158            _,
159            stmt::Expr::Record(stmt::ExprRecord {
160                fields: [
161                    == stmt::Expr::column(user_table.columns[1].id),
162                    == stmt::Expr::column(user_table.columns[2].id),
163                ],
164            }),
165        ]
166    );
167}
168
169/// Tests basic CRUD operations with embedded fields across all ID types.
170/// Validates create, read, update (both instance and query-based), and delete.
171#[driver_test(id(ID))]
172pub async fn create_and_query_embedded(t: &mut Test) -> Result<()> {
173    #[derive(Debug, toasty::Embed)]
174    struct Address {
175        street: String,
176        city: String,
177    }
178
179    #[derive(Debug, toasty::Model)]
180    struct User {
181        #[key]
182        #[auto]
183        id: ID,
184        name: String,
185        address: Address,
186    }
187
188    let mut db = t.setup_db(models!(User, Address)).await;
189
190    let mut user = User::create()
191        .name("Alice")
192        .address(Address {
193            street: "123 Main St".to_string(),
194            city: "Springfield".to_string(),
195        })
196        .exec(&mut db)
197        .await?;
198
199    // Read: embedded struct is reconstructed from flattened columns
200    let found = User::get_by_id(&mut db, &user.id).await?;
201    assert_eq!(found.address.street, "123 Main St");
202    assert_eq!(found.address.city, "Springfield");
203
204    // Update (instance): entire embedded struct can be replaced
205    user.update()
206        .address(Address {
207            street: "456 Oak Ave".to_string(),
208            city: "Shelbyville".to_string(),
209        })
210        .exec(&mut db)
211        .await?;
212
213    let found = User::get_by_id(&mut db, &user.id).await?;
214    assert_eq!(found.address.street, "456 Oak Ave");
215
216    // Update (query-based): tests query builder with embedded fields
217    User::filter_by_id(user.id)
218        .update()
219        .address(Address {
220            street: "789 Pine Rd".to_string(),
221            city: "Capital City".to_string(),
222        })
223        .exec(&mut db)
224        .await?;
225
226    let found = User::get_by_id(&mut db, &user.id).await?;
227    assert_eq!(found.address.street, "789 Pine Rd");
228
229    // Delete: cleanup
230    let id = user.id;
231    user.delete().exec(&mut db).await?;
232    assert_err!(User::get_by_id(&mut db, &id).await);
233    Ok(())
234}
235
236/// Tests code generation for embedded struct field accessors:
237/// - User::fields().address() returns AddressFields
238/// - Chaining works: User::fields().address().city()
239/// - Both model and embedded struct have fields() methods
240/// This is purely a compile-time test validating the generated API.
241#[driver_test]
242pub async fn embedded_struct_fields_codegen(test: &mut Test) {
243    #[derive(Debug, toasty::Embed)]
244    struct Address {
245        street: String,
246        city: String,
247        zip: String,
248    }
249
250    #[derive(Debug, toasty::Model)]
251    #[allow(dead_code)]
252    struct User {
253        #[key]
254        #[auto]
255        id: uuid::Uuid,
256        name: String,
257        address: Address,
258    }
259
260    let _db = test.setup_db(models!(User, Address)).await;
261
262    // Direct chaining: User::fields().address().city()
263    let _city_path = User::fields().address().city();
264
265    // Intermediate variable: AddressFields can be stored and reused
266    let address_fields = User::fields().address();
267    let _city_path_2 = address_fields.city();
268
269    // Embedded struct has its own fields() method
270    let _address_city = Address::fields().city();
271
272    // Paths are usable in filter expressions (compile-time type check)
273    let _query = User::all().filter(User::fields().address().city().eq("Seattle"));
274}
275
276/// Tests querying by embedded struct fields with composite keys (DynamoDB compatible).
277/// Validates:
278/// - Equality queries on embedded fields work across all databases
279/// - Different embedded fields (city, zip) can be queried
280/// - Multiple partition keys work correctly
281/// - Results are properly filtered and returned
282#[driver_test]
283pub async fn query_embedded_struct_fields(t: &mut Test) -> Result<()> {
284    #[derive(Debug, toasty::Embed)]
285    struct Address {
286        street: String,
287        city: String,
288        zip: String,
289    }
290
291    #[derive(Debug, toasty::Model)]
292    #[key(partition = country, local = id)]
293    #[allow(dead_code)]
294    struct User {
295        #[auto]
296        id: uuid::Uuid,
297        country: String,
298        name: String,
299        address: Address,
300    }
301
302    let mut db = t.setup_db(models!(User, Address)).await;
303
304    // Create users in different countries and cities
305    let users_data = [
306        ("USA", "Alice", "123 Main St", "Seattle", "98101"),
307        ("USA", "Bob", "456 Oak Ave", "Seattle", "98102"),
308        ("USA", "Charlie", "789 Pine Rd", "Portland", "97201"),
309        ("USA", "Diana", "321 Elm St", "Portland", "97202"),
310        ("CAN", "Eve", "111 Maple Dr", "Vancouver", "V6B 1A1"),
311        ("CAN", "Frank", "222 Cedar Ln", "Vancouver", "V6B 2B2"),
312        ("CAN", "Grace", "333 Birch Way", "Toronto", "M5H 1A1"),
313    ];
314
315    for (country, name, street, city, zip) in users_data {
316        User::create()
317            .country(country)
318            .name(name)
319            .address(Address {
320                street: street.to_string(),
321                city: city.to_string(),
322                zip: zip.to_string(),
323            })
324            .exec(&mut db)
325            .await?;
326    }
327
328    // Verification: all 7 users were created (DynamoDB requires partition key in queries)
329    let mut all_users = Vec::new();
330    for country in ["USA", "CAN"] {
331        let mut users = User::filter(User::fields().country().eq(country))
332            .exec(&mut db)
333            .await?;
334        all_users.append(&mut users);
335    }
336    assert_eq!(all_users.len(), 7);
337
338    // Core test: query by partition key + embedded field
339    // This tests the projection simplification: address.city -> address_city column
340    let seattle_users = User::filter(
341        User::fields()
342            .country()
343            .eq("USA")
344            .and(User::fields().address().city().eq("Seattle")),
345    )
346    .exec(&mut db)
347    .await?;
348
349    assert_eq!(seattle_users.len(), 2);
350    let mut names: Vec<_> = seattle_users.iter().map(|u| u.name.as_str()).collect();
351    names.sort();
352    assert_eq!(names, ["Alice", "Bob"]);
353
354    // Validate different partition key (CAN) works
355    let vancouver_users = User::filter(
356        User::fields()
357            .country()
358            .eq("CAN")
359            .and(User::fields().address().city().eq("Vancouver")),
360    )
361    .exec(&mut db)
362    .await?;
363
364    assert_eq!(vancouver_users.len(), 2);
365
366    // Validate different embedded field (zip instead of city) works
367    let user_98101 = User::filter(
368        User::fields()
369            .country()
370            .eq("USA")
371            .and(User::fields().address().zip().eq("98101")),
372    )
373    .exec(&mut db)
374    .await?;
375
376    assert_eq!(user_98101.len(), 1);
377    assert_eq!(user_98101[0].name, "Alice");
378    Ok(())
379}
380
381/// Tests comparison operators (gt, lt, ge, le, ne) on embedded struct fields.
382/// SQL-only: DynamoDB doesn't support range queries on non-key attributes.
383/// Validates that all comparison operators work correctly with embedded fields.
384#[driver_test(requires(sql))]
385pub async fn query_embedded_fields_comparison_ops(t: &mut Test) -> Result<()> {
386    #[derive(Debug, toasty::Embed)]
387    struct Stats {
388        score: i64,
389        rank: i64,
390    }
391
392    #[derive(Debug, toasty::Model)]
393    #[allow(dead_code)]
394    struct Player {
395        #[key]
396        #[auto]
397        id: uuid::Uuid,
398        name: String,
399        stats: Stats,
400    }
401
402    let mut db = t.setup_db(models!(Player, Stats)).await;
403
404    for (name, score, rank) in [
405        ("Alice", 100, 1),
406        ("Bob", 85, 2),
407        ("Charlie", 70, 3),
408        ("Diana", 55, 4),
409        ("Eve", 40, 5),
410    ] {
411        Player::create()
412            .name(name)
413            .stats(Stats { score, rank })
414            .exec(&mut db)
415            .await?;
416    }
417
418    // Test gt: score > 80 should return Alice (100) and Bob (85)
419    let high_scorers = Player::filter(Player::fields().stats().score().gt(80))
420        .exec(&mut db)
421        .await?;
422    assert_eq!(high_scorers.len(), 2);
423
424    // Test le: score <= 55 should return Diana (55) and Eve (40)
425    let low_scorers = Player::filter(Player::fields().stats().score().le(55))
426        .exec(&mut db)
427        .await?;
428    assert_eq!(low_scorers.len(), 2);
429
430    // Test ne: score != 70 excludes only Charlie
431    let not_charlie = Player::filter(Player::fields().stats().score().ne(70))
432        .exec(&mut db)
433        .await?;
434    assert_eq!(not_charlie.len(), 4);
435
436    // Test ge: score >= 70 should return Alice, Bob, Charlie
437    let mid_to_high = Player::filter(Player::fields().stats().score().ge(70))
438        .exec(&mut db)
439        .await?;
440    assert_eq!(mid_to_high.len(), 3);
441    Ok(())
442}
443
444/// Tests querying by multiple embedded fields in a single query (AND conditions).
445/// SQL-only: DynamoDB requires partition key in queries.
446/// Validates that complex filters with multiple embedded fields work correctly.
447#[driver_test(requires(sql))]
448pub async fn query_embedded_multiple_fields(t: &mut Test) -> Result<()> {
449    #[derive(Debug, toasty::Embed)]
450    struct Coordinates {
451        x: i64,
452        y: i64,
453        z: i64,
454    }
455
456    #[derive(Debug, toasty::Model)]
457    #[allow(dead_code)]
458    struct Location {
459        #[key]
460        #[auto]
461        id: uuid::Uuid,
462        name: String,
463        coords: Coordinates,
464    }
465
466    let mut db = t.setup_db(models!(Location, Coordinates)).await;
467
468    for (name, x, y, z) in [
469        ("Origin", 0, 0, 0),
470        ("Point A", 10, 20, 0),
471        ("Point B", 10, 30, 0),
472        ("Point C", 10, 20, 5),
473        ("Point D", 20, 20, 0),
474    ] {
475        Location::create()
476            .name(name)
477            .coords(Coordinates { x, y, z })
478            .exec(&mut db)
479            .await?;
480    }
481
482    // Test 2-field AND: x=10 AND y=20 matches Point A (10,20,0) and Point C (10,20,5)
483    let matching = Location::filter(
484        Location::fields()
485            .coords()
486            .x()
487            .eq(10)
488            .and(Location::fields().coords().y().eq(20)),
489    )
490    .exec(&mut db)
491    .await?;
492
493    assert_eq!(matching.len(), 2);
494    let mut names: Vec<_> = matching.iter().map(|l| l.name.as_str()).collect();
495    names.sort();
496    assert_eq!(names, ["Point A", "Point C"]);
497
498    // Test 3-field AND: adding z=0 narrows to just Point A
499    // Validates chaining multiple embedded field conditions
500    let exact_match = Location::filter(
501        Location::fields()
502            .coords()
503            .x()
504            .eq(10)
505            .and(Location::fields().coords().y().eq(20))
506            .and(Location::fields().coords().z().eq(0)),
507    )
508    .exec(&mut db)
509    .await?;
510
511    assert_eq!(exact_match.len(), 1);
512    assert_eq!(exact_match[0].name, "Point A");
513    Ok(())
514}
515
516/// Tests UPDATE operations filtered by embedded struct fields.
517/// SQL-only: DynamoDB requires partition key in queries/updates.
518/// Validates that updates can target rows based on embedded field values.
519#[driver_test(requires(sql))]
520pub async fn update_with_embedded_field_filter(t: &mut Test) -> Result<()> {
521    #[derive(Debug, toasty::Embed)]
522    struct Metadata {
523        version: i64,
524        status: String,
525    }
526
527    #[derive(Debug, toasty::Model)]
528    #[allow(dead_code)]
529    struct Document {
530        #[key]
531        #[auto]
532        id: uuid::Uuid,
533        title: String,
534        meta: Metadata,
535    }
536
537    let mut db = t.setup_db(models!(Document, Metadata)).await;
538
539    // Setup: Doc A (v1, draft), Doc B (v2, draft), Doc C (v1, published)
540    for (title, version, status) in [
541        ("Doc A", 1, "draft"),
542        ("Doc B", 2, "draft"),
543        ("Doc C", 1, "published"),
544    ] {
545        Document::create()
546            .title(title)
547            .meta(Metadata {
548                version,
549                status: status.to_string(),
550            })
551            .exec(&mut db)
552            .await?;
553    }
554
555    // Update documents where status="draft" AND version=1 (should only match Doc A)
556    // Tests that embedded field filters work in UPDATE statements
557    Document::filter(
558        Document::fields()
559            .meta()
560            .status()
561            .eq("draft")
562            .and(Document::fields().meta().version().eq(1)),
563    )
564    .update()
565    .meta(Metadata {
566        version: 2,
567        status: "draft".to_string(),
568    })
569    .exec(&mut db)
570    .await?;
571
572    // Doc A should be updated (was v1 draft, now v2 draft)
573    let doc_a = Document::filter(Document::fields().title().eq("Doc A"))
574        .exec(&mut db)
575        .await?;
576    assert_eq!(doc_a[0].meta.version, 2);
577
578    // Doc B should be unchanged (was v2 draft, still v2 draft)
579    let doc_b = Document::filter(Document::fields().title().eq("Doc B"))
580        .exec(&mut db)
581        .await?;
582    assert_eq!(doc_b[0].meta.version, 2);
583
584    // Doc C should be unchanged (was v1 published, still v1 published - wrong status)
585    let doc_c = Document::filter(Document::fields().title().eq("Doc C"))
586        .exec(&mut db)
587        .await?;
588    assert_eq!(doc_c[0].meta.version, 1);
589    Ok(())
590}
591
592/// Tests partial updates of embedded struct fields using with_field() builders.
593/// This validates that we can update individual fields within an embedded struct
594/// without replacing the entire struct.
595#[driver_test(id(ID))]
596pub async fn partial_update_embedded_fields(t: &mut Test) -> Result<()> {
597    #[derive(Debug, toasty::Embed)]
598    struct Address {
599        street: String,
600        city: String,
601        zip: String,
602    }
603
604    #[derive(Debug, toasty::Model)]
605    struct User {
606        #[key]
607        #[auto]
608        id: ID,
609        name: String,
610        address: Address,
611    }
612
613    let mut db = t.setup_db(models!(User, Address)).await;
614
615    // Create a user with initial address
616    let mut user = User::create()
617        .name("Alice")
618        .address(Address {
619            street: "123 Main St".to_string(),
620            city: "Boston".to_string(),
621            zip: "02101".to_string(),
622        })
623        .exec(&mut db)
624        .await?;
625
626    // Verify initial state
627    assert_struct!(user.address, _ {
628        street: "123 Main St",
629        city: "Boston",
630        zip: "02101",
631        ..
632    });
633
634    // Partial update: only change city, leave street and zip unchanged
635    user.update()
636        .with_address(|a| {
637            a.city("Seattle");
638        })
639        .exec(&mut db)
640        .await?;
641
642    // Verify only city was updated
643    assert_struct!(user.address, _ {
644        street: "123 Main St",
645        city: "Seattle",
646        zip: "02101",
647        ..
648    });
649
650    // Verify the update persisted to database
651    let found = User::get_by_id(&mut db, &user.id).await?;
652    assert_struct!(found.address, _ {
653        street: "123 Main St",
654        city: "Seattle",
655        zip: "02101",
656        ..
657    });
658
659    // Multiple field update in one call
660    user.update()
661        .with_address(|a| {
662            a.city("Portland").zip("97201");
663        })
664        .exec(&mut db)
665        .await?;
666
667    // Verify both fields were updated, street unchanged
668    assert_struct!(user.address, _ {
669        street: "123 Main St",
670        city: "Portland",
671        zip: "97201",
672        ..
673    });
674
675    // Verify the update persisted
676    let found = User::get_by_id(&mut db, &user.id).await?;
677    assert_struct!(found.address, _ {
678        street: "123 Main St",
679        city: "Portland",
680        zip: "97201",
681        ..
682    });
683
684    // Multiple calls to with_address should accumulate
685    user.update()
686        .with_address(|a| {
687            a.street("456 Oak Ave");
688        })
689        .with_address(|a| {
690            a.zip("97202");
691        })
692        .exec(&mut db)
693        .await?;
694
695    // Verify all updates applied in memory
696    assert_struct!(user.address, _ {
697        street: "456 Oak Ave",
698        city: "Portland",
699        zip: "97202",
700        ..
701    });
702
703    // Verify both accumulated assignments persisted to the database
704    let found = User::get_by_id(&mut db, &user.id).await?;
705    assert_struct!(found.address, _ {
706        street: "456 Oak Ave",
707        city: "Portland",
708        zip: "97202",
709        ..
710    });
711    Ok(())
712}
713
714/// Tests deeply nested embedded types (3+ levels) to verify schema building
715/// handles arbitrary nesting depth correctly.
716/// Validates:
717/// - App schema: all embedded models registered
718/// - DB schema: deeply nested fields flattened with proper prefixes
719/// - Mapping: nested Field::Struct structure with correct columns maps
720/// - model_to_table: nested projection expressions
721#[driver_test]
722pub async fn deeply_nested_embedded_schema(test: &mut Test) {
723    // 3 levels of nesting: Location -> City -> Address -> User
724    #[derive(toasty::Embed)]
725    struct Location {
726        lat: i64,
727        lon: i64,
728    }
729
730    #[derive(toasty::Embed)]
731    struct City {
732        name: String,
733        location: Location,
734    }
735
736    #[derive(toasty::Embed)]
737    struct Address {
738        street: String,
739        city: City,
740    }
741
742    #[derive(toasty::Model)]
743    struct User {
744        #[key]
745        id: String,
746        #[allow(dead_code)]
747        address: Address,
748    }
749
750    let db = test.setup_db(models!(User, Address, City, Location)).await;
751    let schema = db.schema();
752
753    // All embedded models should exist in app schema
754    assert_struct!(schema.app.models, #{
755        Location::id(): toasty::schema::app::Model::EmbeddedStruct(_ {
756            name.upper_camel_case(): "Location",
757            fields.len(): 2,
758            ..
759        }),
760        City::id(): toasty::schema::app::Model::EmbeddedStruct(_ {
761            name.upper_camel_case(): "City",
762            fields: [
763                _ { name.app_name: "name", .. },
764                _ {
765                    name.app_name: "location",
766                    ty: FieldTy::Embedded(_ {
767                        target: == Location::id(),
768                        ..
769                    }),
770                    ..
771                },
772            ],
773            ..
774        }),
775        Address::id(): toasty::schema::app::Model::EmbeddedStruct(_ {
776            name.upper_camel_case(): "Address",
777            fields: [
778                _ { name.app_name: "street", .. },
779                _ {
780                    name.app_name: "city",
781                    ty: FieldTy::Embedded(_ {
782                        target: == City::id(),
783                        ..
784                    }),
785                    ..
786                },
787            ],
788            ..
789        }),
790        User::id(): toasty::schema::app::Model::Root(_ {
791            name.upper_camel_case(): "User",
792            fields: [
793                _ { name.app_name: "id", .. },
794                _ {
795                    name.app_name: "address",
796                    ty: FieldTy::Embedded(_ {
797                        target: == Address::id(),
798                        ..
799                    }),
800                    ..
801                },
802            ],
803            ..
804        }),
805    });
806
807    // Database table should flatten all nested fields with proper prefixes
808    // Expected columns:
809    // - id
810    // - address_street
811    // - address_city_name
812    // - address_city_location_lat
813    // - address_city_location_lon
814    assert_struct!(schema.db.tables, [
815        _ {
816            name: =~ r"users$",
817            columns: [
818                _ { name: "id", .. },
819                _ { name: "address_street", .. },
820                _ { name: "address_city_name", .. },
821                _ { name: "address_city_location_lat", .. },
822                _ { name: "address_city_location_lon", .. },
823            ],
824            ..
825        },
826    ]);
827
828    let user = &schema.app.models[&User::id()];
829    let user_table = schema.table_for(user);
830    let user_mapping = &schema.mapping.models[&User::id()];
831
832    // Mapping should have nested Field::Struct structure
833    // User.fields[1] (address) -> FieldStruct {
834    //   fields[0] (street) -> FieldPrimitive { column: address_street }
835    //   fields[1] (city) -> FieldStruct {
836    //     fields[0] (name) -> FieldPrimitive { column: address_city_name }
837    //     fields[1] (location) -> FieldStruct {
838    //       fields[0] (lat) -> FieldPrimitive { column: address_city_location_lat }
839    //       fields[1] (lon) -> FieldPrimitive { column: address_city_location_lon }
840    //     }
841    //   }
842    // }
843
844    assert_eq!(
845        user_mapping.fields.len(),
846        2,
847        "User should have 2 fields: id and address"
848    );
849
850    // Check address field (index 1)
851    let address_field = user_mapping.fields[1]
852        .as_struct()
853        .expect("User.address should be Field::Struct");
854
855    assert_eq!(
856        address_field.fields.len(),
857        2,
858        "Address should have 2 fields: street and city"
859    );
860
861    // Check address.street (index 0)
862    let street_field = address_field.fields[0]
863        .as_primitive()
864        .expect("Address.street should be Field::Primitive");
865    assert_eq!(
866        street_field.column, user_table.columns[1].id,
867        "street should map to address_street column"
868    );
869
870    // Check address.city (index 1)
871    let city_field = address_field.fields[1]
872        .as_struct()
873        .expect("Address.city should be Field::Struct");
874
875    assert_eq!(
876        city_field.fields.len(),
877        2,
878        "City should have 2 fields: name and location"
879    );
880
881    // Check address.city.name (index 0)
882    let city_name_field = city_field.fields[0]
883        .as_primitive()
884        .expect("City.name should be Field::Primitive");
885    assert_eq!(
886        city_name_field.column, user_table.columns[2].id,
887        "city.name should map to address_city_name column"
888    );
889
890    // Check address.city.location (index 1)
891    let location_field = city_field.fields[1]
892        .as_struct()
893        .expect("City.location should be Field::Struct");
894
895    assert_eq!(
896        location_field.fields.len(),
897        2,
898        "Location should have 2 fields: lat and lon"
899    );
900
901    // Check address.city.location.lat (index 0)
902    let lat_field = location_field.fields[0]
903        .as_primitive()
904        .expect("Location.lat should be Field::Primitive");
905    assert_eq!(
906        lat_field.column, user_table.columns[3].id,
907        "location.lat should map to address_city_location_lat column"
908    );
909
910    // Check address.city.location.lon (index 1)
911    let lon_field = location_field.fields[1]
912        .as_primitive()
913        .expect("Location.lon should be Field::Primitive");
914    assert_eq!(
915        lon_field.column, user_table.columns[4].id,
916        "location.lon should map to address_city_location_lon column"
917    );
918
919    // Check that the columns map is correctly populated at each level
920    // Address level should contain all 4 columns (street, city_name, city_location_lat, city_location_lon)
921    assert_eq!(
922        address_field.columns.len(),
923        4,
924        "Address.columns should have 4 entries"
925    );
926    assert!(
927        address_field
928            .columns
929            .contains_key(&user_table.columns[1].id),
930        "Address.columns should contain address_street"
931    );
932    assert!(
933        address_field
934            .columns
935            .contains_key(&user_table.columns[2].id),
936        "Address.columns should contain address_city_name"
937    );
938    assert!(
939        address_field
940            .columns
941            .contains_key(&user_table.columns[3].id),
942        "Address.columns should contain address_city_location_lat"
943    );
944    assert!(
945        address_field
946            .columns
947            .contains_key(&user_table.columns[4].id),
948        "Address.columns should contain address_city_location_lon"
949    );
950
951    // City level should contain 3 columns (name, location_lat, location_lon)
952    assert_eq!(
953        city_field.columns.len(),
954        3,
955        "City.columns should have 3 entries"
956    );
957    assert!(
958        city_field.columns.contains_key(&user_table.columns[2].id),
959        "City.columns should contain address_city_name"
960    );
961    assert!(
962        city_field.columns.contains_key(&user_table.columns[3].id),
963        "City.columns should contain address_city_location_lat"
964    );
965    assert!(
966        city_field.columns.contains_key(&user_table.columns[4].id),
967        "City.columns should contain address_city_location_lon"
968    );
969
970    // Location level should contain 2 columns (lat, lon)
971    assert_eq!(
972        location_field.columns.len(),
973        2,
974        "Location.columns should have 2 entries"
975    );
976    assert!(
977        location_field
978            .columns
979            .contains_key(&user_table.columns[3].id),
980        "Location.columns should contain address_city_location_lat"
981    );
982    assert!(
983        location_field
984            .columns
985            .contains_key(&user_table.columns[4].id),
986        "Location.columns should contain address_city_location_lon"
987    );
988
989    // Verify model_to_table has correct nested projection expressions
990    // Should have 5 expressions: id, address.street, address.city.name, address.city.location.lat, address.city.location.lon
991    assert_eq!(
992        user_mapping.model_to_table.len(),
993        5,
994        "model_to_table should have 5 expressions"
995    );
996
997    // Expression for address.street should be: project(ref(address_field), [0])
998    assert_struct!(
999        user_mapping.model_to_table[1],
1000        == stmt::Expr::project(
1001            stmt::Expr::ref_self_field(user.expect_root().fields[1].id),
1002            [0],
1003        )
1004    );
1005
1006    // Expression for address.city.name should be: project(ref(address_field), [1, 0])
1007    assert_struct!(
1008        user_mapping.model_to_table[2],
1009        == stmt::Expr::project(
1010            stmt::Expr::ref_self_field(user.expect_root().fields[1].id),
1011            [1, 0],
1012        )
1013    );
1014
1015    // Expression for address.city.location.lat should be: project(ref(address_field), [1, 1, 0])
1016    assert_struct!(
1017        user_mapping.model_to_table[3],
1018        == stmt::Expr::project(
1019            stmt::Expr::ref_self_field(user.expect_root().fields[1].id),
1020            [1, 1, 0],
1021        )
1022    );
1023
1024    // Expression for address.city.location.lon should be: project(ref(address_field), [1, 1, 1])
1025    assert_struct!(
1026        user_mapping.model_to_table[4],
1027        == stmt::Expr::project(
1028            stmt::Expr::ref_self_field(user.expect_root().fields[1].id),
1029            [1, 1, 1],
1030        )
1031    );
1032}
1033
1034/// Tests CRUD operations with 2-level nested embedded structs.
1035/// Validates that creating, reading, updating (instance and query-based),
1036/// and deleting records with nested embedded structs works end-to-end.
1037#[driver_test(id(ID))]
1038pub async fn crud_nested_embedded(t: &mut Test) -> Result<()> {
1039    #[derive(Debug, toasty::Embed)]
1040    struct Address {
1041        street: String,
1042        city: String,
1043    }
1044
1045    #[derive(Debug, toasty::Embed)]
1046    struct Office {
1047        name: String,
1048        address: Address,
1049    }
1050
1051    #[derive(Debug, toasty::Model)]
1052    struct Company {
1053        #[key]
1054        #[auto]
1055        id: ID,
1056        name: String,
1057        headquarters: Office,
1058    }
1059
1060    let mut db = t.setup_db(models!(Company, Office, Address)).await;
1061
1062    // Create: nested embedded structs are flattened into a single row
1063    let mut company = Company::create()
1064        .name("Acme")
1065        .headquarters(Office {
1066            name: "Main Office".to_string(),
1067            address: Address {
1068                street: "123 Main St".to_string(),
1069                city: "Springfield".to_string(),
1070            },
1071        })
1072        .exec(&mut db)
1073        .await?;
1074
1075    assert_struct!(company.headquarters, _ {
1076        name: "Main Office",
1077        address: _ {
1078            street: "123 Main St",
1079            city: "Springfield",
1080            ..
1081        },
1082        ..
1083    });
1084
1085    // Read: nested embedded struct is reconstructed from flattened columns
1086    let found = Company::get_by_id(&mut db, &company.id).await?;
1087    assert_struct!(found.headquarters, _ {
1088        name: "Main Office",
1089        address: _ {
1090            street: "123 Main St",
1091            city: "Springfield",
1092            ..
1093        },
1094        ..
1095    });
1096
1097    // Update (instance): replace the entire nested embedded struct
1098    company
1099        .update()
1100        .headquarters(Office {
1101            name: "West Coast HQ".to_string(),
1102            address: Address {
1103                street: "456 Oak Ave".to_string(),
1104                city: "Seattle".to_string(),
1105            },
1106        })
1107        .exec(&mut db)
1108        .await?;
1109
1110    let found = Company::get_by_id(&mut db, &company.id).await?;
1111    assert_struct!(found.headquarters, _ {
1112        name: "West Coast HQ",
1113        address: _ {
1114            street: "456 Oak Ave",
1115            city: "Seattle",
1116            ..
1117        },
1118        ..
1119    });
1120
1121    // Update (query-based): replace nested struct via filter
1122    Company::filter_by_id(company.id)
1123        .update()
1124        .headquarters(Office {
1125            name: "East Coast HQ".to_string(),
1126            address: Address {
1127                street: "789 Pine Rd".to_string(),
1128                city: "Boston".to_string(),
1129            },
1130        })
1131        .exec(&mut db)
1132        .await?;
1133
1134    let found = Company::get_by_id(&mut db, &company.id).await?;
1135    assert_struct!(found.headquarters, _ {
1136        name: "East Coast HQ",
1137        address: _ {
1138            street: "789 Pine Rd",
1139            city: "Boston",
1140            ..
1141        },
1142        ..
1143    });
1144
1145    // Delete: cleanup
1146    let id = company.id;
1147    company.delete().exec(&mut db).await?;
1148    assert_err!(Company::get_by_id(&mut db, &id).await);
1149    Ok(())
1150}
1151
1152/// Tests partial updates of deeply nested embedded fields using chained closures.
1153/// Validates that `with_outer(|o| o.with_inner(|i| i.field(v)))` updates only
1154/// the targeted leaf field, leaving all other fields unchanged in the database.
1155#[driver_test(id(ID))]
1156pub async fn partial_update_nested_embedded(t: &mut Test) -> Result<()> {
1157    #[derive(Debug, toasty::Embed)]
1158    struct Address {
1159        street: String,
1160        city: String,
1161    }
1162
1163    #[derive(Debug, toasty::Embed)]
1164    struct Office {
1165        name: String,
1166        address: Address,
1167    }
1168
1169    #[derive(Debug, toasty::Model)]
1170    struct Company {
1171        #[key]
1172        #[auto]
1173        id: ID,
1174        name: String,
1175        headquarters: Office,
1176    }
1177
1178    let mut db = t.setup_db(models!(Company, Office, Address)).await;
1179
1180    let mut company = Company::create()
1181        .name("Acme")
1182        .headquarters(Office {
1183            name: "Main Office".to_string(),
1184            address: Address {
1185                street: "123 Main St".to_string(),
1186                city: "Boston".to_string(),
1187            },
1188        })
1189        .exec(&mut db)
1190        .await?;
1191
1192    // Nested partial update: change only the city inside headquarters.address.
1193    // street and headquarters.name must remain unchanged.
1194    company
1195        .update()
1196        .with_headquarters(|h| {
1197            h.with_address(|a| {
1198                a.city("Seattle");
1199            });
1200        })
1201        .exec(&mut db)
1202        .await?;
1203
1204    let found = Company::get_by_id(&mut db, &company.id).await?;
1205    assert_struct!(found.headquarters, _ {
1206        name: "Main Office",
1207        address: _ {
1208            street: "123 Main St",
1209            city: "Seattle",
1210            ..
1211        },
1212        ..
1213    });
1214
1215    // Partial update at the outer level: change only headquarters.name.
1216    // address fields must remain unchanged.
1217    company
1218        .update()
1219        .with_headquarters(|h| {
1220            h.name("West Coast HQ");
1221        })
1222        .exec(&mut db)
1223        .await?;
1224
1225    let found = Company::get_by_id(&mut db, &company.id).await?;
1226    assert_struct!(found.headquarters, _ {
1227        name: "West Coast HQ",
1228        address: _ {
1229            street: "123 Main St",
1230            city: "Seattle",
1231            ..
1232        },
1233        ..
1234    });
1235
1236    // Combined update: change headquarters.name and headquarters.address.city
1237    // in a single with_headquarters call. street must remain unchanged.
1238    company
1239        .update()
1240        .with_headquarters(|h| {
1241            h.name("East Coast HQ").with_address(|a| {
1242                a.city("Boston");
1243            });
1244        })
1245        .exec(&mut db)
1246        .await?;
1247
1248    let found = Company::get_by_id(&mut db, &company.id).await?;
1249    assert_struct!(found.headquarters, _ {
1250        name: "East Coast HQ",
1251        address: _ {
1252            street: "123 Main St",
1253            city: "Boston",
1254            ..
1255        },
1256        ..
1257    });
1258    Ok(())
1259}
1260
1261/// Tests partial updates of embedded fields using the query/filter-based path.
1262/// `User::filter_by_id(id).update().with_address(...)` follows a different code path
1263/// than the instance-based `user.update().with_address(...)`, so both need coverage.
1264#[driver_test(id(ID))]
1265pub async fn query_based_partial_update_embedded(t: &mut Test) -> Result<()> {
1266    #[derive(Debug, toasty::Embed)]
1267    struct Address {
1268        street: String,
1269        city: String,
1270        zip: String,
1271    }
1272
1273    #[derive(Debug, toasty::Model)]
1274    struct User {
1275        #[key]
1276        #[auto]
1277        id: ID,
1278        name: String,
1279        address: Address,
1280    }
1281
1282    let mut db = t.setup_db(models!(User, Address)).await;
1283
1284    let user = User::create()
1285        .name("Alice")
1286        .address(Address {
1287            street: "123 Main St".to_string(),
1288            city: "Boston".to_string(),
1289            zip: "02101".to_string(),
1290        })
1291        .exec(&mut db)
1292        .await?;
1293
1294    // Single field: filter-based partial update targeting only city.
1295    // street and zip must remain unchanged.
1296    User::filter_by_id(user.id)
1297        .update()
1298        .with_address(|a| {
1299            a.city("Seattle");
1300        })
1301        .exec(&mut db)
1302        .await?;
1303
1304    let found = User::get_by_id(&mut db, &user.id).await?;
1305    assert_struct!(found.address, _ {
1306        street: "123 Main St",
1307        city: "Seattle",
1308        zip: "02101",
1309        ..
1310    });
1311
1312    // Multiple fields: update city and zip together, leave street unchanged.
1313    User::filter_by_id(user.id)
1314        .update()
1315        .with_address(|a| {
1316            a.city("Portland").zip("97201");
1317        })
1318        .exec(&mut db)
1319        .await?;
1320
1321    let found = User::get_by_id(&mut db, &user.id).await?;
1322    assert_struct!(found.address, _ {
1323        street: "123 Main St",
1324        city: "Portland",
1325        zip: "97201",
1326        ..
1327    });
1328    Ok(())
1329}
1330
1331/// Tests that jiff temporal types inside embedded structs round-trip correctly.
1332/// Covers Timestamp (epoch nanos), civil::Date, civil::Time, and civil::DateTime.
1333#[driver_test(id(ID))]
1334pub async fn embedded_struct_with_jiff_fields(t: &mut Test) -> Result<()> {
1335    #[derive(Debug, toasty::Embed)]
1336    struct Schedule {
1337        starts_at: jiff::Timestamp,
1338        due_date: jiff::civil::Date,
1339        reminder_time: jiff::civil::Time,
1340        scheduled_at: jiff::civil::DateTime,
1341    }
1342
1343    #[derive(Debug, toasty::Model)]
1344    struct Event {
1345        #[key]
1346        #[auto]
1347        id: ID,
1348        name: String,
1349        schedule: Schedule,
1350    }
1351
1352    let mut db = t.setup_db(models!(Event, Schedule)).await;
1353
1354    let starts_at = jiff::Timestamp::from_second(1_700_000_000).unwrap();
1355    let due_date = jiff::civil::date(2025, 6, 15);
1356    let reminder_time = jiff::civil::time(9, 30, 0, 0);
1357    let scheduled_at = jiff::civil::datetime(2025, 6, 15, 9, 30, 0, 0);
1358
1359    let event = Event::create()
1360        .name("team sync")
1361        .schedule(Schedule {
1362            starts_at,
1363            due_date,
1364            reminder_time,
1365            scheduled_at,
1366        })
1367        .exec(&mut db)
1368        .await?;
1369
1370    let found = Event::get_by_id(&mut db, &event.id).await?;
1371    assert_struct!(found.schedule, _ {
1372        starts_at: == starts_at,
1373        due_date: == due_date,
1374        reminder_time: == reminder_time,
1375        scheduled_at: == scheduled_at,
1376    });
1377    Ok(())
1378}
1379
1380/// Tests a unit enum embedded as a field inside an embedded struct (enum-in-struct nesting).
1381/// The struct flattens to columns including the enum's discriminant column.
1382#[driver_test(id(ID))]
1383pub async fn unit_enum_in_embedded_struct(t: &mut Test) -> Result<()> {
1384    #[derive(Debug, PartialEq, toasty::Embed)]
1385    enum Priority {
1386        #[column(variant = 1)]
1387        Low,
1388        #[column(variant = 2)]
1389        Normal,
1390        #[column(variant = 3)]
1391        High,
1392    }
1393
1394    #[derive(Debug, toasty::Embed)]
1395    struct Meta {
1396        label: String,
1397        priority: Priority,
1398    }
1399
1400    #[derive(Debug, toasty::Model)]
1401    struct Task {
1402        #[key]
1403        #[auto]
1404        id: ID,
1405        meta: Meta,
1406    }
1407
1408    let mut db = t.setup_db(models!(Task, Meta, Priority)).await;
1409
1410    let mut task = Task::create()
1411        .meta(Meta {
1412            label: "fix bug".to_string(),
1413            priority: Priority::High,
1414        })
1415        .exec(&mut db)
1416        .await?;
1417
1418    let found = Task::get_by_id(&mut db, &task.id).await?;
1419    assert_eq!(found.meta.label, "fix bug");
1420    assert_eq!(found.meta.priority, Priority::High);
1421
1422    task.update()
1423        .with_meta(|m| {
1424            m.priority(Priority::Normal);
1425        })
1426        .exec(&mut db)
1427        .await?;
1428
1429    let found = Task::get_by_id(&mut db, &task.id).await?;
1430    assert_eq!(found.meta.priority, Priority::Normal);
1431
1432    Ok(())
1433}
1434
1435/// Tests that UUID fields inside embedded structs round-trip correctly.
1436/// UUID requires a type cast on databases that don't support it natively
1437/// (e.g., SQLite stores it as text). This exercises the table_to_model
1438/// lifting path for embedded struct fields with non-trivial type mappings.
1439#[driver_test(id(ID))]
1440pub async fn embedded_struct_with_uuid_field(t: &mut Test) -> Result<()> {
1441    #[derive(Debug, toasty::Embed)]
1442    struct Meta {
1443        ref_id: Uuid,
1444        label: String,
1445    }
1446
1447    #[derive(Debug, toasty::Model)]
1448    struct Item {
1449        #[key]
1450        #[auto]
1451        id: ID,
1452        name: String,
1453        meta: Meta,
1454    }
1455
1456    let mut db = t.setup_db(models!(Item, Meta)).await;
1457
1458    let ref_id = Uuid::new_v4();
1459
1460    let item = Item::create()
1461        .name("widget")
1462        .meta(Meta {
1463            ref_id,
1464            label: "v1".to_string(),
1465        })
1466        .exec(&mut db)
1467        .await?;
1468
1469    // Read back and verify the UUID survived the round-trip
1470    let found = Item::get_by_id(&mut db, &item.id).await?;
1471    assert_eq!(found.meta.ref_id, ref_id);
1472    assert_eq!(found.meta.label, "v1");
1473
1474    Ok(())
1475}