Skip to main content

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