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