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]
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 via `stmt::patch` /
586/// `stmt::apply`. This validates that individual fields within an embedded
587/// struct can be updated 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        .address(toasty::stmt::patch(Address::fields().city(), "Seattle"))
629        .exec(&mut db)
630        .await?;
631
632    // Verify only city was updated
633    assert_struct!(user.address, {
634        street: "123 Main St",
635        city: "Seattle",
636        zip: "02101",
637    });
638
639    // Verify the update persisted to database
640    let found = User::get_by_id(&mut db, &user.id).await?;
641    assert_struct!(found.address, {
642        street: "123 Main St",
643        city: "Seattle",
644        zip: "02101",
645    });
646
647    // Multiple field update in one call
648    user.update()
649        .address(toasty::stmt::apply([
650            toasty::stmt::patch(Address::fields().city(), "Portland"),
651            toasty::stmt::patch(Address::fields().zip(), "97201"),
652        ]))
653        .exec(&mut db)
654        .await?;
655
656    // Verify both fields were updated, street unchanged
657    assert_struct!(user.address, {
658        street: "123 Main St",
659        city: "Portland",
660        zip: "97201",
661    });
662
663    // Verify the update persisted
664    let found = User::get_by_id(&mut db, &user.id).await?;
665    assert_struct!(found.address, {
666        street: "123 Main St",
667        city: "Portland",
668        zip: "97201",
669    });
670
671    // Multiple calls to the address setter should accumulate
672    user.update()
673        .address(toasty::stmt::patch(
674            Address::fields().street(),
675            "456 Oak Ave",
676        ))
677        .address(toasty::stmt::patch(Address::fields().zip(), "97202"))
678        .exec(&mut db)
679        .await?;
680
681    // Verify all updates applied in memory
682    assert_struct!(user.address, {
683        street: "456 Oak Ave",
684        city: "Portland",
685        zip: "97202",
686    });
687
688    // Verify both accumulated assignments persisted to the database
689    let found = User::get_by_id(&mut db, &user.id).await?;
690    assert_struct!(found.address, {
691        street: "456 Oak Ave",
692        city: "Portland",
693        zip: "97202",
694    });
695    Ok(())
696}
697
698/// Tests deeply nested embedded types (3+ levels) to verify schema building
699/// handles arbitrary nesting depth correctly.
700/// Validates:
701/// - App schema: all embedded models registered
702/// - DB schema: deeply nested fields flattened with proper prefixes
703/// - Mapping: nested Field::Struct structure with correct columns maps
704/// - model_to_table: nested projection expressions
705#[driver_test]
706pub async fn deeply_nested_embedded_schema(test: &mut Test) {
707    // 3 levels of nesting: Location -> City -> Address -> User
708    #[derive(toasty::Embed)]
709    struct Location {
710        lat: i64,
711        lon: i64,
712    }
713
714    #[derive(toasty::Embed)]
715    struct City {
716        name: String,
717        location: Location,
718    }
719
720    #[derive(toasty::Embed)]
721    struct Address {
722        street: String,
723        city: City,
724    }
725
726    #[derive(toasty::Model)]
727    struct User {
728        #[key]
729        id: String,
730        #[allow(dead_code)]
731        address: Address,
732    }
733
734    let db = test.setup_db(models!(User, Address, City, Location)).await;
735    let schema = db.schema();
736
737    // All embedded models should exist in app schema
738    assert_struct!(schema.app.models, #{
739        Location::id(): toasty::schema::app::Model::EmbeddedStruct({
740            name.upper_camel_case(): "Location",
741            fields.len(): 2,
742        }),
743        City::id(): toasty::schema::app::Model::EmbeddedStruct({
744            name.upper_camel_case(): "City",
745            fields: [
746                { name.app: Some("name") },
747                {
748                    name.app: Some("location"),
749                    ty: FieldTy::Embedded({
750                        target: == Location::id(),
751                    }),
752                },
753            ],
754        }),
755        Address::id(): toasty::schema::app::Model::EmbeddedStruct({
756            name.upper_camel_case(): "Address",
757            fields: [
758                { name.app: Some("street") },
759                {
760                    name.app: Some("city"),
761                    ty: FieldTy::Embedded({
762                        target: == City::id(),
763                    }),
764                },
765            ],
766        }),
767        User::id(): toasty::schema::app::Model::Root({
768            name.upper_camel_case(): "User",
769            fields: [
770                { name.app: Some("id") },
771                {
772                    name.app: Some("address"),
773                    ty: FieldTy::Embedded({
774                        target: == Address::id(),
775                    }),
776                },
777            ],
778        }),
779    });
780
781    // Database table should flatten all nested fields with proper prefixes
782    // Expected columns:
783    // - id
784    // - address_street
785    // - address_city_name
786    // - address_city_location_lat
787    // - address_city_location_lon
788    assert_struct!(schema.db.tables, [
789        {
790            name: =~ r"users$",
791            columns: [
792                { name: "id" },
793                { name: "address_street" },
794                { name: "address_city_name" },
795                { name: "address_city_location_lat" },
796                { name: "address_city_location_lon" },
797            ],
798        },
799    ]);
800
801    let user = &schema.app.models[&User::id()];
802    let user_table = schema.table_for(user);
803    let user_mapping = &schema.mapping.models[&User::id()];
804
805    // Mapping should have nested Field::Struct structure
806    // User.fields[1] (address) -> FieldStruct {
807    //   fields[0] (street) -> FieldPrimitive { column: address_street }
808    //   fields[1] (city) -> FieldStruct {
809    //     fields[0] (name) -> FieldPrimitive { column: address_city_name }
810    //     fields[1] (location) -> FieldStruct {
811    //       fields[0] (lat) -> FieldPrimitive { column: address_city_location_lat }
812    //       fields[1] (lon) -> FieldPrimitive { column: address_city_location_lon }
813    //     }
814    //   }
815    // }
816
817    assert_eq!(
818        user_mapping.fields.len(),
819        2,
820        "User should have 2 fields: id and address"
821    );
822
823    // Check address field (index 1)
824    let address_field = user_mapping.fields[1]
825        .as_struct()
826        .expect("User.address should be Field::Struct");
827
828    assert_eq!(
829        address_field.fields.len(),
830        2,
831        "Address should have 2 fields: street and city"
832    );
833
834    // Check address.street (index 0)
835    let street_field = address_field.fields[0]
836        .as_primitive()
837        .expect("Address.street should be Field::Primitive");
838    assert_eq!(
839        street_field.column, user_table.columns[1].id,
840        "street should map to address_street column"
841    );
842
843    // Check address.city (index 1)
844    let city_field = address_field.fields[1]
845        .as_struct()
846        .expect("Address.city should be Field::Struct");
847
848    assert_eq!(
849        city_field.fields.len(),
850        2,
851        "City should have 2 fields: name and location"
852    );
853
854    // Check address.city.name (index 0)
855    let city_name_field = city_field.fields[0]
856        .as_primitive()
857        .expect("City.name should be Field::Primitive");
858    assert_eq!(
859        city_name_field.column, user_table.columns[2].id,
860        "city.name should map to address_city_name column"
861    );
862
863    // Check address.city.location (index 1)
864    let location_field = city_field.fields[1]
865        .as_struct()
866        .expect("City.location should be Field::Struct");
867
868    assert_eq!(
869        location_field.fields.len(),
870        2,
871        "Location should have 2 fields: lat and lon"
872    );
873
874    // Check address.city.location.lat (index 0)
875    let lat_field = location_field.fields[0]
876        .as_primitive()
877        .expect("Location.lat should be Field::Primitive");
878    assert_eq!(
879        lat_field.column, user_table.columns[3].id,
880        "location.lat should map to address_city_location_lat column"
881    );
882
883    // Check address.city.location.lon (index 1)
884    let lon_field = location_field.fields[1]
885        .as_primitive()
886        .expect("Location.lon should be Field::Primitive");
887    assert_eq!(
888        lon_field.column, user_table.columns[4].id,
889        "location.lon should map to address_city_location_lon column"
890    );
891
892    // Check that the columns map is correctly populated at each level
893    // Address level should contain all 4 columns (street, city_name, city_location_lat, city_location_lon)
894    assert_eq!(
895        address_field.columns.len(),
896        4,
897        "Address.columns should have 4 entries"
898    );
899    assert!(
900        address_field
901            .columns
902            .contains_key(&user_table.columns[1].id),
903        "Address.columns should contain address_street"
904    );
905    assert!(
906        address_field
907            .columns
908            .contains_key(&user_table.columns[2].id),
909        "Address.columns should contain address_city_name"
910    );
911    assert!(
912        address_field
913            .columns
914            .contains_key(&user_table.columns[3].id),
915        "Address.columns should contain address_city_location_lat"
916    );
917    assert!(
918        address_field
919            .columns
920            .contains_key(&user_table.columns[4].id),
921        "Address.columns should contain address_city_location_lon"
922    );
923
924    // City level should contain 3 columns (name, location_lat, location_lon)
925    assert_eq!(
926        city_field.columns.len(),
927        3,
928        "City.columns should have 3 entries"
929    );
930    assert!(
931        city_field.columns.contains_key(&user_table.columns[2].id),
932        "City.columns should contain address_city_name"
933    );
934    assert!(
935        city_field.columns.contains_key(&user_table.columns[3].id),
936        "City.columns should contain address_city_location_lat"
937    );
938    assert!(
939        city_field.columns.contains_key(&user_table.columns[4].id),
940        "City.columns should contain address_city_location_lon"
941    );
942
943    // Location level should contain 2 columns (lat, lon)
944    assert_eq!(
945        location_field.columns.len(),
946        2,
947        "Location.columns should have 2 entries"
948    );
949    assert!(
950        location_field
951            .columns
952            .contains_key(&user_table.columns[3].id),
953        "Location.columns should contain address_city_location_lat"
954    );
955    assert!(
956        location_field
957            .columns
958            .contains_key(&user_table.columns[4].id),
959        "Location.columns should contain address_city_location_lon"
960    );
961
962    // Verify model_to_table has correct nested projection expressions
963    // Should have 5 expressions: id, address.street, address.city.name, address.city.location.lat, address.city.location.lon
964    assert_eq!(
965        user_mapping.model_to_table.len(),
966        5,
967        "model_to_table should have 5 expressions"
968    );
969
970    // Expression for address.street should be: project(ref(address_field), [0])
971    assert_struct!(
972        user_mapping.model_to_table[1],
973        == stmt::Expr::project(
974            stmt::Expr::ref_self_field(user.as_root_unwrap().fields[1].id),
975            [0],
976        )
977    );
978
979    // Expression for address.city.name should be: project(ref(address_field), [1, 0])
980    assert_struct!(
981        user_mapping.model_to_table[2],
982        == stmt::Expr::project(
983            stmt::Expr::ref_self_field(user.as_root_unwrap().fields[1].id),
984            [1, 0],
985        )
986    );
987
988    // Expression for address.city.location.lat should be: project(ref(address_field), [1, 1, 0])
989    assert_struct!(
990        user_mapping.model_to_table[3],
991        == stmt::Expr::project(
992            stmt::Expr::ref_self_field(user.as_root_unwrap().fields[1].id),
993            [1, 1, 0],
994        )
995    );
996
997    // Expression for address.city.location.lon should be: project(ref(address_field), [1, 1, 1])
998    assert_struct!(
999        user_mapping.model_to_table[4],
1000        == stmt::Expr::project(
1001            stmt::Expr::ref_self_field(user.as_root_unwrap().fields[1].id),
1002            [1, 1, 1],
1003        )
1004    );
1005}
1006
1007/// Tests CRUD operations with 2-level nested embedded structs.
1008/// Validates that creating, reading, updating (instance and query-based),
1009/// and deleting records with nested embedded structs works end-to-end.
1010#[driver_test(id(ID))]
1011pub async fn crud_nested_embedded(t: &mut Test) -> Result<()> {
1012    #[derive(Debug, toasty::Embed)]
1013    struct Address {
1014        street: String,
1015        city: String,
1016    }
1017
1018    #[derive(Debug, toasty::Embed)]
1019    struct Office {
1020        name: String,
1021        address: Address,
1022    }
1023
1024    #[derive(Debug, toasty::Model)]
1025    struct Company {
1026        #[key]
1027        #[auto]
1028        id: ID,
1029        name: String,
1030        headquarters: Office,
1031    }
1032
1033    let mut db = t.setup_db(models!(Company, Office, Address)).await;
1034
1035    // Create: nested embedded structs are flattened into a single row
1036    let mut company = Company::create()
1037        .name("Acme")
1038        .headquarters(Office {
1039            name: "Main Office".to_string(),
1040            address: Address {
1041                street: "123 Main St".to_string(),
1042                city: "Springfield".to_string(),
1043            },
1044        })
1045        .exec(&mut db)
1046        .await?;
1047
1048    assert_struct!(company.headquarters, {
1049        name: "Main Office",
1050        address: {
1051            street: "123 Main St",
1052            city: "Springfield",
1053        },
1054    });
1055
1056    // Read: nested embedded struct is reconstructed from flattened columns
1057    let found = Company::get_by_id(&mut db, &company.id).await?;
1058    assert_struct!(found.headquarters, {
1059        name: "Main Office",
1060        address: {
1061            street: "123 Main St",
1062            city: "Springfield",
1063        },
1064    });
1065
1066    // Update (instance): replace the entire nested embedded struct
1067    company
1068        .update()
1069        .headquarters(Office {
1070            name: "West Coast HQ".to_string(),
1071            address: Address {
1072                street: "456 Oak Ave".to_string(),
1073                city: "Seattle".to_string(),
1074            },
1075        })
1076        .exec(&mut db)
1077        .await?;
1078
1079    let found = Company::get_by_id(&mut db, &company.id).await?;
1080    assert_struct!(found.headquarters, {
1081        name: "West Coast HQ",
1082        address: {
1083            street: "456 Oak Ave",
1084            city: "Seattle",
1085        },
1086    });
1087
1088    // Update (query-based): replace nested struct via filter
1089    Company::filter_by_id(company.id)
1090        .update()
1091        .headquarters(Office {
1092            name: "East Coast HQ".to_string(),
1093            address: Address {
1094                street: "789 Pine Rd".to_string(),
1095                city: "Boston".to_string(),
1096            },
1097        })
1098        .exec(&mut db)
1099        .await?;
1100
1101    let found = Company::get_by_id(&mut db, &company.id).await?;
1102    assert_struct!(found.headquarters, {
1103        name: "East Coast HQ",
1104        address: {
1105            street: "789 Pine Rd",
1106            city: "Boston",
1107        },
1108    });
1109
1110    // Delete: cleanup
1111    let id = company.id;
1112    company.delete().exec(&mut db).await?;
1113    assert_err!(Company::get_by_id(&mut db, &id).await);
1114    Ok(())
1115}
1116
1117/// Tests partial updates of deeply nested embedded fields via nested
1118/// `stmt::patch` calls. Validates that patching a leaf field inside an
1119/// outer embedded struct updates only that leaf, leaving all other fields
1120/// unchanged in the database.
1121#[driver_test(id(ID))]
1122pub async fn partial_update_nested_embedded(t: &mut Test) -> Result<()> {
1123    #[derive(Debug, toasty::Embed)]
1124    struct Address {
1125        street: String,
1126        city: String,
1127    }
1128
1129    #[derive(Debug, toasty::Embed)]
1130    struct Office {
1131        name: String,
1132        address: Address,
1133    }
1134
1135    #[derive(Debug, toasty::Model)]
1136    struct Company {
1137        #[key]
1138        #[auto]
1139        id: ID,
1140        name: String,
1141        headquarters: Office,
1142    }
1143
1144    let mut db = t.setup_db(models!(Company, Office, Address)).await;
1145
1146    let mut company = Company::create()
1147        .name("Acme")
1148        .headquarters(Office {
1149            name: "Main Office".to_string(),
1150            address: Address {
1151                street: "123 Main St".to_string(),
1152                city: "Boston".to_string(),
1153            },
1154        })
1155        .exec(&mut db)
1156        .await?;
1157
1158    // Nested partial update: change only the city inside headquarters.address.
1159    // street and headquarters.name must remain unchanged.
1160    company
1161        .update()
1162        .headquarters(toasty::stmt::patch(
1163            Office::fields().address().city(),
1164            "Seattle",
1165        ))
1166        .exec(&mut db)
1167        .await?;
1168
1169    let found = Company::get_by_id(&mut db, &company.id).await?;
1170    assert_struct!(found.headquarters, {
1171        name: "Main Office",
1172        address: {
1173            street: "123 Main St",
1174            city: "Seattle",
1175        },
1176    });
1177
1178    // Partial update at the outer level: change only headquarters.name.
1179    // address fields must remain unchanged.
1180    company
1181        .update()
1182        .headquarters(toasty::stmt::patch(
1183            Office::fields().name(),
1184            "West Coast HQ",
1185        ))
1186        .exec(&mut db)
1187        .await?;
1188
1189    let found = Company::get_by_id(&mut db, &company.id).await?;
1190    assert_struct!(found.headquarters, {
1191        name: "West Coast HQ",
1192        address: {
1193            street: "123 Main St",
1194            city: "Seattle",
1195        },
1196    });
1197
1198    // Combined update: change headquarters.name and headquarters.address.city
1199    // in a single call via stmt::apply. street must remain unchanged.
1200    company
1201        .update()
1202        .headquarters(toasty::stmt::apply([
1203            toasty::stmt::patch(Office::fields().name(), "East Coast HQ"),
1204            toasty::stmt::patch(Office::fields().address().city(), "Boston"),
1205        ]))
1206        .exec(&mut db)
1207        .await?;
1208
1209    let found = Company::get_by_id(&mut db, &company.id).await?;
1210    assert_struct!(found.headquarters, {
1211        name: "East Coast HQ",
1212        address: {
1213            street: "123 Main St",
1214            city: "Boston",
1215        },
1216    });
1217    Ok(())
1218}
1219
1220/// Tests partial updates of embedded fields using the query/filter-based path.
1221/// `User::filter_by_id(id).update().address(stmt::patch(...))` follows a different
1222/// code path than the instance-based `user.update().address(stmt::patch(...))`,
1223/// so both need coverage.
1224#[driver_test(id(ID))]
1225pub async fn query_based_partial_update_embedded(t: &mut Test) -> Result<()> {
1226    #[derive(Debug, toasty::Embed)]
1227    struct Address {
1228        street: String,
1229        city: String,
1230        zip: String,
1231    }
1232
1233    #[derive(Debug, toasty::Model)]
1234    struct User {
1235        #[key]
1236        #[auto]
1237        id: ID,
1238        name: String,
1239        address: Address,
1240    }
1241
1242    let mut db = t.setup_db(models!(User, Address)).await;
1243
1244    let user = User::create()
1245        .name("Alice")
1246        .address(Address {
1247            street: "123 Main St".to_string(),
1248            city: "Boston".to_string(),
1249            zip: "02101".to_string(),
1250        })
1251        .exec(&mut db)
1252        .await?;
1253
1254    // Single field: filter-based partial update targeting only city.
1255    // street and zip must remain unchanged.
1256    User::filter_by_id(user.id)
1257        .update()
1258        .address(toasty::stmt::patch(Address::fields().city(), "Seattle"))
1259        .exec(&mut db)
1260        .await?;
1261
1262    let found = User::get_by_id(&mut db, &user.id).await?;
1263    assert_struct!(found.address, {
1264        street: "123 Main St",
1265        city: "Seattle",
1266        zip: "02101",
1267    });
1268
1269    // Multiple fields: update city and zip together, leave street unchanged.
1270    User::filter_by_id(user.id)
1271        .update()
1272        .address(toasty::stmt::apply([
1273            toasty::stmt::patch(Address::fields().city(), "Portland"),
1274            toasty::stmt::patch(Address::fields().zip(), "97201"),
1275        ]))
1276        .exec(&mut db)
1277        .await?;
1278
1279    let found = User::get_by_id(&mut db, &user.id).await?;
1280    assert_struct!(found.address, {
1281        street: "123 Main St",
1282        city: "Portland",
1283        zip: "97201",
1284    });
1285    Ok(())
1286}
1287
1288/// Tests that jiff temporal types inside embedded structs round-trip correctly.
1289/// Covers Timestamp (epoch nanos), civil::Date, civil::Time, and civil::DateTime.
1290#[driver_test(id(ID))]
1291pub async fn embedded_struct_with_jiff_fields(t: &mut Test) -> Result<()> {
1292    #[derive(Debug, toasty::Embed)]
1293    struct Schedule {
1294        starts_at: jiff::Timestamp,
1295        due_date: jiff::civil::Date,
1296        reminder_time: jiff::civil::Time,
1297        scheduled_at: jiff::civil::DateTime,
1298    }
1299
1300    #[derive(Debug, toasty::Model)]
1301    struct Event {
1302        #[key]
1303        #[auto]
1304        id: ID,
1305        name: String,
1306        schedule: Schedule,
1307    }
1308
1309    let mut db = t.setup_db(models!(Event, Schedule)).await;
1310
1311    let starts_at = jiff::Timestamp::from_second(1_700_000_000).unwrap();
1312    let due_date = jiff::civil::date(2025, 6, 15);
1313    let reminder_time = jiff::civil::time(9, 30, 0, 0);
1314    let scheduled_at = jiff::civil::datetime(2025, 6, 15, 9, 30, 0, 0);
1315
1316    let event = Event::create()
1317        .name("team sync")
1318        .schedule(Schedule {
1319            starts_at,
1320            due_date,
1321            reminder_time,
1322            scheduled_at,
1323        })
1324        .exec(&mut db)
1325        .await?;
1326
1327    let found = Event::get_by_id(&mut db, &event.id).await?;
1328    assert_struct!(found.schedule, {
1329        starts_at: == starts_at,
1330        due_date: == due_date,
1331        reminder_time: == reminder_time,
1332        scheduled_at: == scheduled_at,
1333    });
1334    Ok(())
1335}
1336
1337/// Tests a unit enum embedded as a field inside an embedded struct (enum-in-struct nesting).
1338/// The struct flattens to columns including the enum's discriminant column.
1339#[driver_test(id(ID))]
1340pub async fn unit_enum_in_embedded_struct(t: &mut Test) -> Result<()> {
1341    #[derive(Debug, PartialEq, toasty::Embed)]
1342    enum Priority {
1343        #[column(variant = 1)]
1344        Low,
1345        #[column(variant = 2)]
1346        Normal,
1347        #[column(variant = 3)]
1348        High,
1349    }
1350
1351    #[derive(Debug, toasty::Embed)]
1352    struct Meta {
1353        label: String,
1354        priority: Priority,
1355    }
1356
1357    #[derive(Debug, toasty::Model)]
1358    struct Task {
1359        #[key]
1360        #[auto]
1361        id: ID,
1362        meta: Meta,
1363    }
1364
1365    let mut db = t.setup_db(models!(Task, Meta, Priority)).await;
1366
1367    let mut task = Task::create()
1368        .meta(Meta {
1369            label: "fix bug".to_string(),
1370            priority: Priority::High,
1371        })
1372        .exec(&mut db)
1373        .await?;
1374
1375    let found = Task::get_by_id(&mut db, &task.id).await?;
1376    assert_eq!(found.meta.label, "fix bug");
1377    assert_eq!(found.meta.priority, Priority::High);
1378
1379    task.update()
1380        .meta(toasty::stmt::patch(
1381            Meta::fields().priority().into(),
1382            Priority::Normal,
1383        ))
1384        .exec(&mut db)
1385        .await?;
1386
1387    let found = Task::get_by_id(&mut db, &task.id).await?;
1388    assert_eq!(found.meta.priority, Priority::Normal);
1389
1390    Ok(())
1391}
1392
1393/// Tests that UUID fields inside embedded structs round-trip correctly.
1394/// UUID requires a type cast on databases that don't support it natively
1395/// (e.g., SQLite stores it as text). This exercises the table_to_model
1396/// lifting path for embedded struct fields with non-trivial type mappings.
1397#[driver_test(id(ID))]
1398pub async fn embedded_struct_with_uuid_field(t: &mut Test) -> Result<()> {
1399    #[derive(Debug, toasty::Embed)]
1400    struct Meta {
1401        ref_id: Uuid,
1402        label: String,
1403    }
1404
1405    #[derive(Debug, toasty::Model)]
1406    struct Item {
1407        #[key]
1408        #[auto]
1409        id: ID,
1410        name: String,
1411        meta: Meta,
1412    }
1413
1414    let mut db = t.setup_db(models!(Item, Meta)).await;
1415
1416    let ref_id = Uuid::new_v4();
1417
1418    let item = Item::create()
1419        .name("widget")
1420        .meta(Meta {
1421            ref_id,
1422            label: "v1".to_string(),
1423        })
1424        .exec(&mut db)
1425        .await?;
1426
1427    // Read back and verify the UUID survived the round-trip
1428    let found = Item::get_by_id(&mut db, &item.id).await?;
1429    assert_eq!(found.meta.ref_id, ref_id);
1430    assert_eq!(found.meta.label, "v1");
1431
1432    Ok(())
1433}