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(id(ID))]
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: ID,
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::id_uuid))]
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 querying by multiple embedded fields in a single query (AND conditions).
397/// SQL-only: DynamoDB requires partition key in queries.
398/// Validates that complex filters with multiple embedded fields work correctly.
399#[driver_test(requires(scan))]
400pub async fn query_embedded_multiple_fields(t: &mut Test) -> Result<()> {
401    #[derive(Debug, toasty::Embed)]
402    struct Coordinates {
403        x: i64,
404        y: i64,
405        z: i64,
406    }
407
408    #[derive(Debug, toasty::Model)]
409    #[allow(dead_code)]
410    struct Location {
411        #[key]
412        #[auto]
413        id: uuid::Uuid,
414        name: String,
415        coords: Coordinates,
416    }
417
418    let mut db = t.setup_db(models!(Location)).await;
419
420    for (name, x, y, z) in [
421        ("Origin", 0, 0, 0),
422        ("Point A", 10, 20, 0),
423        ("Point B", 10, 30, 0),
424        ("Point C", 10, 20, 5),
425        ("Point D", 20, 20, 0),
426    ] {
427        Location::create()
428            .name(name)
429            .coords(Coordinates { x, y, z })
430            .exec(&mut db)
431            .await?;
432    }
433
434    // Test 2-field AND: x=10 AND y=20 matches Point A (10,20,0) and Point C (10,20,5)
435    let matching = Location::filter(
436        Location::fields()
437            .coords()
438            .x()
439            .eq(10)
440            .and(Location::fields().coords().y().eq(20)),
441    )
442    .exec(&mut db)
443    .await?;
444
445    assert_eq!(matching.len(), 2);
446    let mut names: Vec<_> = matching.iter().map(|l| l.name.as_str()).collect();
447    names.sort();
448    assert_eq!(names, ["Point A", "Point C"]);
449
450    // Test 3-field AND: adding z=0 narrows to just Point A
451    // Validates chaining multiple embedded field conditions
452    let exact_match = Location::filter(
453        Location::fields()
454            .coords()
455            .x()
456            .eq(10)
457            .and(Location::fields().coords().y().eq(20))
458            .and(Location::fields().coords().z().eq(0)),
459    )
460    .exec(&mut db)
461    .await?;
462
463    assert_eq!(exact_match.len(), 1);
464    assert_eq!(exact_match[0].name, "Point A");
465    Ok(())
466}
467
468/// Tests UPDATE operations filtered by embedded struct fields.
469/// SQL-only: DynamoDB requires partition key in queries/updates.
470/// Validates that updates can target rows based on embedded field values.
471#[driver_test(requires(sql))]
472pub async fn update_with_embedded_field_filter(t: &mut Test) -> Result<()> {
473    #[derive(Debug, toasty::Embed)]
474    struct Metadata {
475        version: i64,
476        status: String,
477    }
478
479    #[derive(Debug, toasty::Model)]
480    #[allow(dead_code)]
481    struct Document {
482        #[key]
483        #[auto]
484        id: uuid::Uuid,
485        title: String,
486        meta: Metadata,
487    }
488
489    let mut db = t.setup_db(models!(Document)).await;
490
491    // Setup: Doc A (v1, draft), Doc B (v2, draft), Doc C (v1, published)
492    for (title, version, status) in [
493        ("Doc A", 1, "draft"),
494        ("Doc B", 2, "draft"),
495        ("Doc C", 1, "published"),
496    ] {
497        Document::create()
498            .title(title)
499            .meta(Metadata {
500                version,
501                status: status.to_string(),
502            })
503            .exec(&mut db)
504            .await?;
505    }
506
507    // Update documents where status="draft" AND version=1 (should only match Doc A)
508    // Tests that embedded field filters work in UPDATE statements
509    Document::filter(
510        Document::fields()
511            .meta()
512            .status()
513            .eq("draft")
514            .and(Document::fields().meta().version().eq(1)),
515    )
516    .update()
517    .meta(Metadata {
518        version: 2,
519        status: "draft".to_string(),
520    })
521    .exec(&mut db)
522    .await?;
523
524    // Doc A should be updated (was v1 draft, now v2 draft)
525    let doc_a = Document::filter(Document::fields().title().eq("Doc A"))
526        .exec(&mut db)
527        .await?;
528    assert_eq!(doc_a[0].meta.version, 2);
529
530    // Doc B should be unchanged (was v2 draft, still v2 draft)
531    let doc_b = Document::filter(Document::fields().title().eq("Doc B"))
532        .exec(&mut db)
533        .await?;
534    assert_eq!(doc_b[0].meta.version, 2);
535
536    // Doc C should be unchanged (was v1 published, still v1 published - wrong status)
537    let doc_c = Document::filter(Document::fields().title().eq("Doc C"))
538        .exec(&mut db)
539        .await?;
540    assert_eq!(doc_c[0].meta.version, 1);
541    Ok(())
542}
543
544/// Tests partial updates of embedded struct fields via `stmt::patch` /
545/// `stmt::apply`. This validates that individual fields within an embedded
546/// struct can be updated without replacing the entire struct.
547#[driver_test(id(ID), scenario(crate::scenarios::user_with_zip_address))]
548pub async fn partial_update_embedded_fields(t: &mut Test) -> Result<()> {
549    let mut db = setup(t).await;
550
551    // Create a user with initial address
552    let mut user = User::create()
553        .name("Alice")
554        .address(Address {
555            street: "123 Main St".to_string(),
556            city: "Boston".to_string(),
557            zip: "02101".to_string(),
558        })
559        .exec(&mut db)
560        .await?;
561
562    // Verify initial state
563    assert_struct!(user.address, {
564        street: "123 Main St",
565        city: "Boston",
566        zip: "02101",
567    });
568
569    // Partial update: only change city, leave street and zip unchanged
570    user.update()
571        .address(toasty::stmt::patch(Address::fields().city(), "Seattle"))
572        .exec(&mut db)
573        .await?;
574
575    // Verify only city was updated
576    assert_struct!(user.address, {
577        street: "123 Main St",
578        city: "Seattle",
579        zip: "02101",
580    });
581
582    // Verify the update persisted to database
583    let found = User::get_by_id(&mut db, &user.id).await?;
584    assert_struct!(found.address, {
585        street: "123 Main St",
586        city: "Seattle",
587        zip: "02101",
588    });
589
590    // Multiple field update in one call
591    user.update()
592        .address(toasty::stmt::apply([
593            toasty::stmt::patch(Address::fields().city(), "Portland"),
594            toasty::stmt::patch(Address::fields().zip(), "97201"),
595        ]))
596        .exec(&mut db)
597        .await?;
598
599    // Verify both fields were updated, street unchanged
600    assert_struct!(user.address, {
601        street: "123 Main St",
602        city: "Portland",
603        zip: "97201",
604    });
605
606    // Verify the update persisted
607    let found = User::get_by_id(&mut db, &user.id).await?;
608    assert_struct!(found.address, {
609        street: "123 Main St",
610        city: "Portland",
611        zip: "97201",
612    });
613
614    // Multiple calls to the address setter should accumulate
615    user.update()
616        .address(toasty::stmt::patch(
617            Address::fields().street(),
618            "456 Oak Ave",
619        ))
620        .address(toasty::stmt::patch(Address::fields().zip(), "97202"))
621        .exec(&mut db)
622        .await?;
623
624    // Verify all updates applied in memory
625    assert_struct!(user.address, {
626        street: "456 Oak Ave",
627        city: "Portland",
628        zip: "97202",
629    });
630
631    // Verify both accumulated assignments persisted to the database
632    let found = User::get_by_id(&mut db, &user.id).await?;
633    assert_struct!(found.address, {
634        street: "456 Oak Ave",
635        city: "Portland",
636        zip: "97202",
637    });
638    Ok(())
639}
640
641/// Tests deeply nested embedded types (3+ levels) to verify schema building
642/// handles arbitrary nesting depth correctly.
643/// Validates:
644/// - App schema: all embedded models registered
645/// - DB schema: deeply nested fields flattened with proper prefixes
646/// - Mapping: nested Field::Struct structure with correct columns maps
647/// - model_to_table: nested projection expressions
648#[driver_test]
649pub async fn deeply_nested_embedded_schema(test: &mut Test) {
650    // 3 levels of nesting: Location -> City -> Address -> User
651    #[derive(toasty::Embed)]
652    struct Location {
653        lat: i64,
654        lon: i64,
655    }
656
657    #[derive(toasty::Embed)]
658    struct City {
659        name: String,
660        location: Location,
661    }
662
663    #[derive(toasty::Embed)]
664    struct Address {
665        street: String,
666        city: City,
667    }
668
669    #[derive(toasty::Model)]
670    struct User {
671        #[key]
672        id: String,
673        #[allow(dead_code)]
674        address: Address,
675    }
676
677    let db = test.setup_db(models!(User)).await;
678    let schema = db.schema();
679
680    // All embedded models should exist in app schema
681    assert_struct!(schema.app.models, #{
682        Location::id(): toasty::schema::app::Model::EmbeddedStruct({
683            name.upper_camel_case(): "Location",
684            fields.len(): 2,
685        }),
686        City::id(): toasty::schema::app::Model::EmbeddedStruct({
687            name.upper_camel_case(): "City",
688            fields: [
689                { name.app: Some("name") },
690                {
691                    name.app: Some("location"),
692                    ty: FieldTy::Embedded({
693                        target: == Location::id(),
694                    }),
695                },
696            ],
697        }),
698        Address::id(): toasty::schema::app::Model::EmbeddedStruct({
699            name.upper_camel_case(): "Address",
700            fields: [
701                { name.app: Some("street") },
702                {
703                    name.app: Some("city"),
704                    ty: FieldTy::Embedded({
705                        target: == City::id(),
706                    }),
707                },
708            ],
709        }),
710        User::id(): toasty::schema::app::Model::Root({
711            name.upper_camel_case(): "User",
712            fields: [
713                { name.app: Some("id") },
714                {
715                    name.app: Some("address"),
716                    ty: FieldTy::Embedded({
717                        target: == Address::id(),
718                    }),
719                },
720            ],
721        }),
722    });
723
724    // Database table should flatten all nested fields with proper prefixes
725    // Expected columns:
726    // - id
727    // - address_street
728    // - address_city_name
729    // - address_city_location_lat
730    // - address_city_location_lon
731    assert_struct!(schema.db.tables, [
732        {
733            name: =~ r"users$",
734            columns: [
735                { name: "id" },
736                { name: "address_street" },
737                { name: "address_city_name" },
738                { name: "address_city_location_lat" },
739                { name: "address_city_location_lon" },
740            ],
741        },
742    ]);
743
744    let user = &schema.app.models[&User::id()];
745    let user_table = schema.table_for(user);
746    let user_mapping = &schema.mapping.models[&User::id()];
747
748    // Mapping should have nested Field::Struct structure
749    // User.fields[1] (address) -> FieldStruct {
750    //   fields[0] (street) -> FieldPrimitive { column: address_street }
751    //   fields[1] (city) -> FieldStruct {
752    //     fields[0] (name) -> FieldPrimitive { column: address_city_name }
753    //     fields[1] (location) -> FieldStruct {
754    //       fields[0] (lat) -> FieldPrimitive { column: address_city_location_lat }
755    //       fields[1] (lon) -> FieldPrimitive { column: address_city_location_lon }
756    //     }
757    //   }
758    // }
759
760    assert_eq!(
761        user_mapping.fields.len(),
762        2,
763        "User should have 2 fields: id and address"
764    );
765
766    // Check address field (index 1)
767    let address_field = user_mapping.fields[1]
768        .as_struct()
769        .expect("User.address should be Field::Struct");
770
771    assert_eq!(
772        address_field.fields.len(),
773        2,
774        "Address should have 2 fields: street and city"
775    );
776
777    // Check address.street (index 0)
778    let street_field = address_field.fields[0]
779        .as_primitive()
780        .expect("Address.street should be Field::Primitive");
781    assert_eq!(
782        street_field.column, user_table.columns[1].id,
783        "street should map to address_street column"
784    );
785
786    // Check address.city (index 1)
787    let city_field = address_field.fields[1]
788        .as_struct()
789        .expect("Address.city should be Field::Struct");
790
791    assert_eq!(
792        city_field.fields.len(),
793        2,
794        "City should have 2 fields: name and location"
795    );
796
797    // Check address.city.name (index 0)
798    let city_name_field = city_field.fields[0]
799        .as_primitive()
800        .expect("City.name should be Field::Primitive");
801    assert_eq!(
802        city_name_field.column, user_table.columns[2].id,
803        "city.name should map to address_city_name column"
804    );
805
806    // Check address.city.location (index 1)
807    let location_field = city_field.fields[1]
808        .as_struct()
809        .expect("City.location should be Field::Struct");
810
811    assert_eq!(
812        location_field.fields.len(),
813        2,
814        "Location should have 2 fields: lat and lon"
815    );
816
817    // Check address.city.location.lat (index 0)
818    let lat_field = location_field.fields[0]
819        .as_primitive()
820        .expect("Location.lat should be Field::Primitive");
821    assert_eq!(
822        lat_field.column, user_table.columns[3].id,
823        "location.lat should map to address_city_location_lat column"
824    );
825
826    // Check address.city.location.lon (index 1)
827    let lon_field = location_field.fields[1]
828        .as_primitive()
829        .expect("Location.lon should be Field::Primitive");
830    assert_eq!(
831        lon_field.column, user_table.columns[4].id,
832        "location.lon should map to address_city_location_lon column"
833    );
834
835    // Check that the columns map is correctly populated at each level
836    // Address level should contain all 4 columns (street, city_name, city_location_lat, city_location_lon)
837    assert_eq!(
838        address_field.columns.len(),
839        4,
840        "Address.columns should have 4 entries"
841    );
842    assert!(
843        address_field
844            .columns
845            .contains_key(&user_table.columns[1].id),
846        "Address.columns should contain address_street"
847    );
848    assert!(
849        address_field
850            .columns
851            .contains_key(&user_table.columns[2].id),
852        "Address.columns should contain address_city_name"
853    );
854    assert!(
855        address_field
856            .columns
857            .contains_key(&user_table.columns[3].id),
858        "Address.columns should contain address_city_location_lat"
859    );
860    assert!(
861        address_field
862            .columns
863            .contains_key(&user_table.columns[4].id),
864        "Address.columns should contain address_city_location_lon"
865    );
866
867    // City level should contain 3 columns (name, location_lat, location_lon)
868    assert_eq!(
869        city_field.columns.len(),
870        3,
871        "City.columns should have 3 entries"
872    );
873    assert!(
874        city_field.columns.contains_key(&user_table.columns[2].id),
875        "City.columns should contain address_city_name"
876    );
877    assert!(
878        city_field.columns.contains_key(&user_table.columns[3].id),
879        "City.columns should contain address_city_location_lat"
880    );
881    assert!(
882        city_field.columns.contains_key(&user_table.columns[4].id),
883        "City.columns should contain address_city_location_lon"
884    );
885
886    // Location level should contain 2 columns (lat, lon)
887    assert_eq!(
888        location_field.columns.len(),
889        2,
890        "Location.columns should have 2 entries"
891    );
892    assert!(
893        location_field
894            .columns
895            .contains_key(&user_table.columns[3].id),
896        "Location.columns should contain address_city_location_lat"
897    );
898    assert!(
899        location_field
900            .columns
901            .contains_key(&user_table.columns[4].id),
902        "Location.columns should contain address_city_location_lon"
903    );
904
905    // Verify model_to_table has correct nested projection expressions
906    // Should have 5 expressions: id, address.street, address.city.name, address.city.location.lat, address.city.location.lon
907    assert_eq!(
908        user_mapping.model_to_table.len(),
909        5,
910        "model_to_table should have 5 expressions"
911    );
912
913    // Expression for address.street should be: project(ref(address_field), [0])
914    assert_struct!(
915        user_mapping.model_to_table[1],
916        == stmt::Expr::project(
917            stmt::Expr::ref_self_field(user.as_root_unwrap().fields[1].id),
918            [0],
919        )
920    );
921
922    // Expression for address.city.name should be: project(ref(address_field), [1, 0])
923    assert_struct!(
924        user_mapping.model_to_table[2],
925        == stmt::Expr::project(
926            stmt::Expr::ref_self_field(user.as_root_unwrap().fields[1].id),
927            [1, 0],
928        )
929    );
930
931    // Expression for address.city.location.lat should be: project(ref(address_field), [1, 1, 0])
932    assert_struct!(
933        user_mapping.model_to_table[3],
934        == stmt::Expr::project(
935            stmt::Expr::ref_self_field(user.as_root_unwrap().fields[1].id),
936            [1, 1, 0],
937        )
938    );
939
940    // Expression for address.city.location.lon should be: project(ref(address_field), [1, 1, 1])
941    assert_struct!(
942        user_mapping.model_to_table[4],
943        == stmt::Expr::project(
944            stmt::Expr::ref_self_field(user.as_root_unwrap().fields[1].id),
945            [1, 1, 1],
946        )
947    );
948}
949
950/// Tests CRUD operations with 2-level nested embedded structs.
951/// Validates that creating, reading, updating (instance and query-based),
952/// and deleting records with nested embedded structs works end-to-end.
953#[driver_test(id(ID), scenario(crate::scenarios::company_office_address))]
954pub async fn crud_nested_embedded(t: &mut Test) -> Result<()> {
955    let mut db = setup(t).await;
956
957    // Create: nested embedded structs are flattened into a single row
958    let mut company = Company::create()
959        .name("Acme")
960        .headquarters(Office {
961            name: "Main Office".to_string(),
962            address: Address {
963                street: "123 Main St".to_string(),
964                city: "Springfield".to_string(),
965            },
966        })
967        .exec(&mut db)
968        .await?;
969
970    assert_struct!(company.headquarters, {
971        name: "Main Office",
972        address: {
973            street: "123 Main St",
974            city: "Springfield",
975        },
976    });
977
978    // Read: nested embedded struct is reconstructed from flattened columns
979    let found = Company::get_by_id(&mut db, &company.id).await?;
980    assert_struct!(found.headquarters, {
981        name: "Main Office",
982        address: {
983            street: "123 Main St",
984            city: "Springfield",
985        },
986    });
987
988    // Update (instance): replace the entire nested embedded struct
989    company
990        .update()
991        .headquarters(Office {
992            name: "West Coast HQ".to_string(),
993            address: Address {
994                street: "456 Oak Ave".to_string(),
995                city: "Seattle".to_string(),
996            },
997        })
998        .exec(&mut db)
999        .await?;
1000
1001    let found = Company::get_by_id(&mut db, &company.id).await?;
1002    assert_struct!(found.headquarters, {
1003        name: "West Coast HQ",
1004        address: {
1005            street: "456 Oak Ave",
1006            city: "Seattle",
1007        },
1008    });
1009
1010    // Update (query-based): replace nested struct via filter
1011    Company::filter_by_id(company.id)
1012        .update()
1013        .headquarters(Office {
1014            name: "East Coast HQ".to_string(),
1015            address: Address {
1016                street: "789 Pine Rd".to_string(),
1017                city: "Boston".to_string(),
1018            },
1019        })
1020        .exec(&mut db)
1021        .await?;
1022
1023    let found = Company::get_by_id(&mut db, &company.id).await?;
1024    assert_struct!(found.headquarters, {
1025        name: "East Coast HQ",
1026        address: {
1027            street: "789 Pine Rd",
1028            city: "Boston",
1029        },
1030    });
1031
1032    // Delete: cleanup
1033    let id = company.id;
1034    company.delete().exec(&mut db).await?;
1035    assert_err!(Company::get_by_id(&mut db, &id).await);
1036    Ok(())
1037}
1038
1039/// Tests partial updates of deeply nested embedded fields via nested
1040/// `stmt::patch` calls. Validates that patching a leaf field inside an
1041/// outer embedded struct updates only that leaf, leaving all other fields
1042/// unchanged in the database.
1043#[driver_test(id(ID), scenario(crate::scenarios::company_office_address))]
1044pub async fn partial_update_nested_embedded(t: &mut Test) -> Result<()> {
1045    let mut db = setup(t).await;
1046
1047    let mut company = Company::create()
1048        .name("Acme")
1049        .headquarters(Office {
1050            name: "Main Office".to_string(),
1051            address: Address {
1052                street: "123 Main St".to_string(),
1053                city: "Boston".to_string(),
1054            },
1055        })
1056        .exec(&mut db)
1057        .await?;
1058
1059    // Nested partial update: change only the city inside headquarters.address.
1060    // street and headquarters.name must remain unchanged.
1061    company
1062        .update()
1063        .headquarters(toasty::stmt::patch(
1064            Office::fields().address().city(),
1065            "Seattle",
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: "Main Office",
1073        address: {
1074            street: "123 Main St",
1075            city: "Seattle",
1076        },
1077    });
1078
1079    // Partial update at the outer level: change only headquarters.name.
1080    // address fields must remain unchanged.
1081    company
1082        .update()
1083        .headquarters(toasty::stmt::patch(
1084            Office::fields().name(),
1085            "West Coast HQ",
1086        ))
1087        .exec(&mut db)
1088        .await?;
1089
1090    let found = Company::get_by_id(&mut db, &company.id).await?;
1091    assert_struct!(found.headquarters, {
1092        name: "West Coast HQ",
1093        address: {
1094            street: "123 Main St",
1095            city: "Seattle",
1096        },
1097    });
1098
1099    // Combined update: change headquarters.name and headquarters.address.city
1100    // in a single call via stmt::apply. street must remain unchanged.
1101    company
1102        .update()
1103        .headquarters(toasty::stmt::apply([
1104            toasty::stmt::patch(Office::fields().name(), "East Coast HQ"),
1105            toasty::stmt::patch(Office::fields().address().city(), "Boston"),
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: "East Coast HQ",
1113        address: {
1114            street: "123 Main St",
1115            city: "Boston",
1116        },
1117    });
1118    Ok(())
1119}
1120
1121/// Tests partial updates of embedded fields using the query/filter-based path.
1122/// `User::filter_by_id(id).update().address(stmt::patch(...))` follows a different
1123/// code path than the instance-based `user.update().address(stmt::patch(...))`,
1124/// so both need coverage.
1125#[driver_test(id(ID), scenario(crate::scenarios::user_with_zip_address))]
1126pub async fn query_based_partial_update_embedded(t: &mut Test) -> Result<()> {
1127    let mut db = setup(t).await;
1128
1129    let user = User::create()
1130        .name("Alice")
1131        .address(Address {
1132            street: "123 Main St".to_string(),
1133            city: "Boston".to_string(),
1134            zip: "02101".to_string(),
1135        })
1136        .exec(&mut db)
1137        .await?;
1138
1139    // Single field: filter-based partial update targeting only city.
1140    // street and zip must remain unchanged.
1141    User::filter_by_id(user.id)
1142        .update()
1143        .address(toasty::stmt::patch(Address::fields().city(), "Seattle"))
1144        .exec(&mut db)
1145        .await?;
1146
1147    let found = User::get_by_id(&mut db, &user.id).await?;
1148    assert_struct!(found.address, {
1149        street: "123 Main St",
1150        city: "Seattle",
1151        zip: "02101",
1152    });
1153
1154    // Multiple fields: update city and zip together, leave street unchanged.
1155    User::filter_by_id(user.id)
1156        .update()
1157        .address(toasty::stmt::apply([
1158            toasty::stmt::patch(Address::fields().city(), "Portland"),
1159            toasty::stmt::patch(Address::fields().zip(), "97201"),
1160        ]))
1161        .exec(&mut db)
1162        .await?;
1163
1164    let found = User::get_by_id(&mut db, &user.id).await?;
1165    assert_struct!(found.address, {
1166        street: "123 Main St",
1167        city: "Portland",
1168        zip: "97201",
1169    });
1170    Ok(())
1171}
1172
1173/// Tests that jiff temporal types inside embedded structs round-trip correctly.
1174/// Covers Timestamp (epoch nanos), civil::Date, civil::Time, and civil::DateTime.
1175#[driver_test(id(ID))]
1176pub async fn embedded_struct_with_jiff_fields(t: &mut Test) -> Result<()> {
1177    #[derive(Debug, toasty::Embed)]
1178    struct Schedule {
1179        starts_at: jiff::Timestamp,
1180        due_date: jiff::civil::Date,
1181        reminder_time: jiff::civil::Time,
1182        scheduled_at: jiff::civil::DateTime,
1183    }
1184
1185    #[derive(Debug, toasty::Model)]
1186    struct Event {
1187        #[key]
1188        #[auto]
1189        id: ID,
1190        name: String,
1191        schedule: Schedule,
1192    }
1193
1194    let mut db = t.setup_db(models!(Event)).await;
1195
1196    let starts_at = jiff::Timestamp::from_second(1_700_000_000).unwrap();
1197    let due_date = jiff::civil::date(2025, 6, 15);
1198    let reminder_time = jiff::civil::time(9, 30, 0, 0);
1199    let scheduled_at = jiff::civil::datetime(2025, 6, 15, 9, 30, 0, 0);
1200
1201    let event = Event::create()
1202        .name("team sync")
1203        .schedule(Schedule {
1204            starts_at,
1205            due_date,
1206            reminder_time,
1207            scheduled_at,
1208        })
1209        .exec(&mut db)
1210        .await?;
1211
1212    let found = Event::get_by_id(&mut db, &event.id).await?;
1213    assert_struct!(found.schedule, {
1214        starts_at: == starts_at,
1215        due_date: == due_date,
1216        reminder_time: == reminder_time,
1217        scheduled_at: == scheduled_at,
1218    });
1219    Ok(())
1220}
1221
1222/// Tests a unit enum embedded as a field inside an embedded struct (enum-in-struct nesting).
1223/// The struct flattens to columns including the enum's discriminant column.
1224#[driver_test(id(ID))]
1225pub async fn unit_enum_in_embedded_struct(t: &mut Test) -> Result<()> {
1226    #[derive(Debug, PartialEq, toasty::Embed)]
1227    enum Priority {
1228        #[column(variant = 1)]
1229        Low,
1230        #[column(variant = 2)]
1231        Normal,
1232        #[column(variant = 3)]
1233        High,
1234    }
1235
1236    #[derive(Debug, toasty::Embed)]
1237    struct Meta {
1238        label: String,
1239        priority: Priority,
1240    }
1241
1242    #[derive(Debug, toasty::Model)]
1243    struct Task {
1244        #[key]
1245        #[auto]
1246        id: ID,
1247        meta: Meta,
1248    }
1249
1250    let mut db = t.setup_db(models!(Task)).await;
1251
1252    let mut task = Task::create()
1253        .meta(Meta {
1254            label: "fix bug".to_string(),
1255            priority: Priority::High,
1256        })
1257        .exec(&mut db)
1258        .await?;
1259
1260    let found = Task::get_by_id(&mut db, &task.id).await?;
1261    assert_eq!(found.meta.label, "fix bug");
1262    assert_eq!(found.meta.priority, Priority::High);
1263
1264    task.update()
1265        .meta(toasty::stmt::patch(
1266            Meta::fields().priority().into(),
1267            Priority::Normal,
1268        ))
1269        .exec(&mut db)
1270        .await?;
1271
1272    let found = Task::get_by_id(&mut db, &task.id).await?;
1273    assert_eq!(found.meta.priority, Priority::Normal);
1274
1275    Ok(())
1276}
1277
1278/// Tests that UUID fields inside embedded structs round-trip correctly.
1279/// UUID requires a type cast on databases that don't support it natively
1280/// (e.g., SQLite stores it as text). This exercises the table_to_model
1281/// lifting path for embedded struct fields with non-trivial type mappings.
1282#[driver_test(id(ID))]
1283pub async fn embedded_struct_with_uuid_field(t: &mut Test) -> Result<()> {
1284    #[derive(Debug, toasty::Embed)]
1285    struct Meta {
1286        ref_id: Uuid,
1287        label: String,
1288    }
1289
1290    #[derive(Debug, toasty::Model)]
1291    struct Item {
1292        #[key]
1293        #[auto]
1294        id: ID,
1295        name: String,
1296        meta: Meta,
1297    }
1298
1299    let mut db = t.setup_db(models!(Item)).await;
1300
1301    let ref_id = Uuid::new_v4();
1302
1303    let item = Item::create()
1304        .name("widget")
1305        .meta(Meta {
1306            ref_id,
1307            label: "v1".to_string(),
1308        })
1309        .exec(&mut db)
1310        .await?;
1311
1312    // Read back and verify the UUID survived the round-trip
1313    let found = Item::get_by_id(&mut db, &item.id).await?;
1314    assert_eq!(found.meta.ref_id, ref_id);
1315    assert_eq!(found.meta.label, "v1");
1316
1317    Ok(())
1318}