Skip to main content

toasty_driver_integration_suite/tests/
embed_enum_data.rs

1use crate::prelude::*;
2
3/// Verifies that a data-carrying enum has its variant fields registered in the app
4/// schema with globally-assigned field indices (indices are unique across all variants).
5#[driver_test(scenario(crate::scenarios::user_contact_info))]
6pub async fn data_carrying_enum_schema(t: &mut Test) {
7    let db = setup(t).await;
8    let schema = db.schema();
9
10    let contact_info = &schema.app.models[&ContactInfo::id()];
11    assert_struct!(contact_info, toasty::schema::app::Model::EmbeddedEnum({
12        name.upper_camel_case(): "ContactInfo",
13        variants: [
14            {
15                name.upper_camel_case(): "Email",
16                discriminant: toasty_core::stmt::Value::I64(1),
17            },
18            {
19                name.upper_camel_case(): "Phone",
20                discriminant: toasty_core::stmt::Value::I64(2),
21            },
22        ],
23        fields: [
24            { id.index: 0, name.app: Some("address") },
25            { id.index: 1, name.app: Some("number") },
26        ],
27    }));
28}
29
30/// Verifies that a mixed enum (some unit variants, some data variants) registers
31/// correctly: unit variants have empty `fields`, data variants have their fields
32/// with indices assigned starting from 0 and continuing globally across variants.
33#[driver_test(scenario(crate::scenarios::task_with_status))]
34pub async fn mixed_enum_schema(t: &mut Test) {
35    let db = setup(t).await;
36    let schema = db.schema();
37
38    let status = &schema.app.models[&Status::id()];
39    assert_struct!(status, toasty::schema::app::Model::EmbeddedEnum({
40        variants: [
41            {
42                name.upper_camel_case(): "Pending",
43                discriminant: toasty_core::stmt::Value::I64(1),
44            },
45            {
46                name.upper_camel_case(): "Failed",
47                discriminant: toasty_core::stmt::Value::I64(2),
48            },
49            {
50                name.upper_camel_case(): "Done",
51                discriminant: toasty_core::stmt::Value::I64(3),
52            },
53        ],
54        fields: [
55            { id.index: 0, name.app: Some("reason") },
56        ],
57    }));
58}
59
60/// Verifies DB columns for a data-carrying enum: discriminant column + one nullable
61/// column per variant field, named `{disc_col}_{field_name}`.
62#[driver_test]
63pub async fn data_carrying_enum_db_schema(test: &mut Test) {
64    #[allow(dead_code)]
65    #[derive(toasty::Embed)]
66    enum ContactInfo {
67        #[column(variant = 1)]
68        Email { address: String },
69        #[column(variant = 2)]
70        Phone { number: String },
71    }
72
73    #[derive(toasty::Model)]
74    struct User {
75        #[key]
76        id: String,
77        #[allow(dead_code)]
78        contact: ContactInfo,
79    }
80
81    let db = test.setup_db(models!(User)).await;
82    let schema = db.schema();
83
84    // The DB table has disc col + one col per variant field (2 variants × 1 field each).
85    assert_struct!(schema.db.tables, [
86        {
87            name: =~ r"users$",
88            columns: [
89                { name: "id" },
90                { name: "contact", nullable: false },
91                { name: "contact_address", nullable: true },
92                { name: "contact_number", nullable: true },
93            ],
94        },
95    ]);
96}
97
98/// An integer-discriminant enum can request a narrower database column while
99/// keeping its application-level discriminant representation as `i64`.
100#[driver_test]
101pub async fn integer_discriminant_storage_type(test: &mut Test) -> Result<()> {
102    #[derive(Debug, PartialEq, toasty::Embed)]
103    #[column(type = u16)]
104    enum ContactMethod {
105        #[column(variant = 1)]
106        Email { address: String },
107        #[column(variant = 2)]
108        Phone { number: String },
109        #[column(variant = 3)]
110        DoNotContact,
111    }
112
113    #[derive(Debug, toasty::Model)]
114    struct Contact {
115        #[key]
116        id: String,
117        #[column(type = u8)]
118        method: ContactMethod,
119    }
120
121    let mut db = test.setup_db(models!(Contact)).await;
122
123    assert_struct!(db.schema().db.tables, [
124        {
125            name: =~ r"contacts$",
126            columns: [
127                { name: "id" },
128                {
129                    name: "method",
130                    ty: toasty_core::stmt::Type::U8,
131                    storage_ty: toasty_core::schema::db::Type::UnsignedInteger(1),
132                },
133                { name: "method_address" },
134                { name: "method_number" },
135            ],
136        },
137    ]);
138
139    let dana = toasty::create!(Contact {
140        id: "dana",
141        method: ContactMethod::Email {
142            address: "d@e.com".to_string(),
143        },
144    })
145    .exec(&mut db)
146    .await?;
147
148    let anonymous = toasty::create!(Contact {
149        id: "anonymous",
150        method: ContactMethod::DoNotContact,
151    })
152    .exec(&mut db)
153    .await?;
154
155    assert_eq!(
156        Contact::get_by_id(&mut db, &dana.id).await?.method,
157        ContactMethod::Email {
158            address: "d@e.com".to_string(),
159        }
160    );
161    assert_eq!(
162        Contact::get_by_id(&mut db, &anonymous.id).await?.method,
163        ContactMethod::DoNotContact
164    );
165
166    Ok(())
167}
168
169/// End-to-end CRUD test for a data-carrying enum (all variants have fields).
170/// Creates records with different variants, reads them back, and verifies roundtrip.
171#[driver_test(scenario(crate::scenarios::user_contact_info))]
172pub async fn data_variant_roundtrip(test: &mut Test) -> Result<()> {
173    let mut db = setup(test).await;
174
175    let alice = User::create()
176        .name("Alice")
177        .contact(ContactInfo::Email {
178            address: "alice@example.com".to_string(),
179        })
180        .exec(&mut db)
181        .await?;
182
183    let bob = User::create()
184        .name("Bob")
185        .contact(ContactInfo::Phone {
186            number: "555-1234".to_string(),
187        })
188        .exec(&mut db)
189        .await?;
190
191    // Read back and check values are reconstructed correctly.
192    let found_alice = User::get_by_id(&mut db, &alice.id).await?;
193    assert_eq!(
194        found_alice.contact,
195        ContactInfo::Email {
196            address: "alice@example.com".to_string()
197        }
198    );
199
200    let found_bob = User::get_by_id(&mut db, &bob.id).await?;
201    assert_eq!(
202        found_bob.contact,
203        ContactInfo::Phone {
204            number: "555-1234".to_string()
205        }
206    );
207
208    // Clean up.
209    alice.delete().exec(&mut db).await?;
210    bob.delete().exec(&mut db).await?;
211    Ok(())
212}
213
214/// End-to-end CRUD test for a mixed enum (unit variants and data variants).
215/// Verifies that both kinds round-trip correctly through the DB.
216#[driver_test(scenario(crate::scenarios::task_with_status))]
217pub async fn mixed_enum_roundtrip(t: &mut Test) -> Result<()> {
218    let mut db = setup(t).await;
219
220    let pending = Task::create()
221        .title("Pending task")
222        .status(Status::Pending)
223        .exec(&mut db)
224        .await?;
225
226    let failed = Task::create()
227        .title("Failed task")
228        .status(Status::Failed {
229            reason: "out of memory".to_string(),
230        })
231        .exec(&mut db)
232        .await?;
233
234    let done = Task::create()
235        .title("Done task")
236        .status(Status::Done)
237        .exec(&mut db)
238        .await?;
239
240    let found_pending = Task::get_by_id(&mut db, &pending.id).await?;
241    assert_eq!(found_pending.status, Status::Pending);
242
243    let found_failed = Task::get_by_id(&mut db, &failed.id).await?;
244    assert_eq!(
245        found_failed.status,
246        Status::Failed {
247            reason: "out of memory".to_string()
248        }
249    );
250
251    let found_done = Task::get_by_id(&mut db, &done.id).await?;
252    assert_eq!(found_done.status, Status::Done);
253
254    Ok(())
255}
256
257/// Updating a mixed enum field from a data-carrying variant to a unit variant
258/// (and back) round-trips correctly. Regression test for #1068: the unit
259/// variant's value record is narrower than the data variant's data column
260/// expects, which used to panic while lowering the update.
261#[driver_test(scenario(crate::scenarios::task_with_status))]
262pub async fn mixed_enum_update_data_to_unit_variant(t: &mut Test) -> Result<()> {
263    let mut db = setup(t).await;
264
265    let mut task = toasty::create!(Task {
266        title: "task",
267        status: Status::Failed {
268            reason: "boom".to_string(),
269        },
270    })
271    .exec(&mut db)
272    .await?;
273
274    // Data variant -> unit variant (the reported panic).
275    task.update().status(Status::Pending).exec(&mut db).await?;
276    assert_eq!(
277        Task::get_by_id(&mut db, &task.id).await?.status,
278        Status::Pending
279    );
280
281    // Unit variant -> data variant, to confirm the reverse still works.
282    task.update()
283        .status(Status::Failed {
284            reason: "again".to_string(),
285        })
286        .exec(&mut db)
287        .await?;
288    assert_eq!(
289        Task::get_by_id(&mut db, &task.id).await?.status,
290        Status::Failed {
291            reason: "again".to_string()
292        }
293    );
294
295    Ok(())
296}
297
298/// Updating a data-carrying enum between variants with *different field counts*
299/// round-trips. The narrower variant's value record is shorter than the wider
300/// variant's data columns expect, exercising the same out-of-bounds projection
301/// path as #1068 without any unit variant involved.
302#[driver_test]
303pub async fn enum_update_between_variants_of_different_width(test: &mut Test) -> Result<()> {
304    #[derive(Debug, PartialEq, toasty::Embed)]
305    enum Event {
306        #[column(variant = 1)]
307        Login { user: String },
308        #[column(variant = 2)]
309        Purchase { item: String, amount: i64 },
310    }
311
312    #[derive(Debug, toasty::Model)]
313    struct Log {
314        #[key]
315        #[auto]
316        id: uuid::Uuid,
317        event: Event,
318    }
319
320    let mut db = test.setup_db(models!(Log)).await;
321
322    let mut log = toasty::create!(Log {
323        event: Event::Purchase {
324            item: "book".to_string(),
325            amount: 42,
326        },
327    })
328    .exec(&mut db)
329    .await?;
330
331    // Wider variant -> narrower variant.
332    log.update()
333        .event(Event::Login {
334            user: "alice".to_string(),
335        })
336        .exec(&mut db)
337        .await?;
338    assert_eq!(
339        Log::get_by_id(&mut db, &log.id).await?.event,
340        Event::Login {
341            user: "alice".to_string()
342        }
343    );
344
345    Ok(())
346}
347
348/// Tests that UUID fields inside data-carrying enum variants round-trip correctly.
349/// UUID is a non-trivial primitive that requires type casting on some databases.
350#[driver_test]
351pub async fn data_variant_with_uuid_field(test: &mut Test) -> Result<()> {
352    #[derive(Debug, PartialEq, toasty::Embed)]
353    enum OrderRef {
354        #[column(variant = 1)]
355        Internal { id: uuid::Uuid },
356        #[column(variant = 2)]
357        External { code: String },
358    }
359
360    #[derive(Debug, toasty::Model)]
361    struct Order {
362        #[key]
363        #[auto]
364        id: uuid::Uuid,
365        order_ref: OrderRef,
366    }
367
368    let mut db = test.setup_db(models!(Order)).await;
369
370    let internal_id = uuid::Uuid::new_v4();
371
372    let o1 = Order::create()
373        .order_ref(OrderRef::Internal { id: internal_id })
374        .exec(&mut db)
375        .await?;
376
377    let o2 = Order::create()
378        .order_ref(OrderRef::External {
379            code: "EXT-001".to_string(),
380        })
381        .exec(&mut db)
382        .await?;
383
384    let found_o1 = Order::get_by_id(&mut db, &o1.id).await?;
385    assert_eq!(found_o1.order_ref, OrderRef::Internal { id: internal_id });
386
387    let found_o2 = Order::get_by_id(&mut db, &o2.id).await?;
388    assert_eq!(
389        found_o2.order_ref,
390        OrderRef::External {
391            code: "EXT-001".to_string()
392        }
393    );
394
395    Ok(())
396}
397
398/// Tests that a `Vec<scalar>` field inside a data-carrying enum variant
399/// round-trips correctly.
400#[driver_test(requires(vec_scalar))]
401pub async fn data_variant_with_vec_scalar_field(test: &mut Test) -> Result<()> {
402    #[derive(Debug, PartialEq, toasty::Embed)]
403    enum HighlightMedia {
404        #[column(variant = 1)]
405        Image { image_urls: Vec<String> },
406        #[column(variant = 2)]
407        Video { video_id: String },
408    }
409
410    #[derive(Debug, toasty::Model)]
411    struct Highlight {
412        #[key]
413        #[auto]
414        id: uuid::Uuid,
415        media: HighlightMedia,
416    }
417
418    let mut db = test.setup_db(models!(Highlight)).await;
419
420    let image_urls = vec!["a.png".to_string(), "b.png".to_string()];
421
422    let h1 = Highlight::create()
423        .media(HighlightMedia::Image {
424            image_urls: image_urls.clone(),
425        })
426        .exec(&mut db)
427        .await?;
428
429    let h2 = Highlight::create()
430        .media(HighlightMedia::Video {
431            video_id: "vid-1".to_string(),
432        })
433        .exec(&mut db)
434        .await?;
435
436    let found_h1 = Highlight::get_by_id(&mut db, &h1.id).await?;
437    assert_eq!(found_h1.media, HighlightMedia::Image { image_urls });
438
439    let found_h2 = Highlight::get_by_id(&mut db, &h2.id).await?;
440    assert_eq!(
441        found_h2.media,
442        HighlightMedia::Video {
443            video_id: "vid-1".to_string()
444        }
445    );
446
447    Ok(())
448}
449
450/// Tests that jiff::Timestamp fields inside data-carrying enum variants round-trip correctly.
451/// Also covers a mixed enum (one unit variant, one data variant) to verify null handling.
452#[driver_test]
453pub async fn data_variant_with_jiff_timestamp(test: &mut Test) -> Result<()> {
454    #[derive(Debug, PartialEq, toasty::Embed)]
455    enum EventTime {
456        #[column(variant = 1)]
457        Scheduled { at: jiff::Timestamp },
458        #[column(variant = 2)]
459        Unscheduled,
460    }
461
462    #[derive(Debug, toasty::Model)]
463    struct Event {
464        #[key]
465        #[auto]
466        id: uuid::Uuid,
467        name: String,
468        time: EventTime,
469    }
470
471    let mut db = test.setup_db(models!(Event)).await;
472
473    let ts = jiff::Timestamp::from_second(1_700_000_000).unwrap();
474
475    let scheduled = Event::create()
476        .name("launch")
477        .time(EventTime::Scheduled { at: ts })
478        .exec(&mut db)
479        .await?;
480
481    let unscheduled = Event::create()
482        .name("tbd")
483        .time(EventTime::Unscheduled)
484        .exec(&mut db)
485        .await?;
486
487    let found_scheduled = Event::get_by_id(&mut db, &scheduled.id).await?;
488    assert_eq!(found_scheduled.time, EventTime::Scheduled { at: ts });
489
490    let found_unscheduled = Event::get_by_id(&mut db, &unscheduled.id).await?;
491    assert_eq!(found_unscheduled.time, EventTime::Unscheduled);
492
493    Ok(())
494}
495
496#[driver_test]
497pub async fn struct_in_data_variant(test: &mut Test) -> Result<()> {
498    #[derive(Debug, PartialEq, toasty::Embed)]
499    struct Address {
500        street: String,
501        city: String,
502    }
503
504    #[derive(Debug, PartialEq, toasty::Embed)]
505    enum Destination {
506        #[column(variant = 1)]
507        Digital { email: String },
508        #[column(variant = 2)]
509        Physical { address: Address },
510    }
511
512    #[derive(Debug, toasty::Model)]
513    struct Shipment {
514        #[key]
515        #[auto]
516        id: uuid::Uuid,
517        destination: Destination,
518    }
519
520    let mut db = test.setup_db(models!(Shipment)).await;
521
522    let digital = Shipment::create()
523        .destination(Destination::Digital {
524            email: "user@example.com".to_string(),
525        })
526        .exec(&mut db)
527        .await?;
528
529    let physical = Shipment::create()
530        .destination(Destination::Physical {
531            address: Address {
532                street: "123 Main St".to_string(),
533                city: "Seattle".to_string(),
534            },
535        })
536        .exec(&mut db)
537        .await?;
538
539    let found_digital = Shipment::get_by_id(&mut db, &digital.id).await?;
540    assert_eq!(
541        found_digital.destination,
542        Destination::Digital {
543            email: "user@example.com".to_string()
544        }
545    );
546
547    let found_physical = Shipment::get_by_id(&mut db, &physical.id).await?;
548    assert_eq!(
549        found_physical.destination,
550        Destination::Physical {
551            address: Address {
552                street: "123 Main St".to_string(),
553                city: "Seattle".to_string(),
554            },
555        }
556    );
557
558    Ok(())
559}
560
561/// Roundtrip test for an enum embedded inside a variant field of another enum (enum-in-enum).
562/// The inner enum is unit-only; the outer has one data variant and one unit variant.
563#[driver_test]
564pub async fn enum_in_enum_roundtrip(test: &mut Test) -> Result<()> {
565    #[derive(Debug, PartialEq, toasty::Embed)]
566    enum Channel {
567        #[column(variant = 1)]
568        Email,
569        #[column(variant = 2)]
570        Sms,
571    }
572
573    #[derive(Debug, PartialEq, toasty::Embed)]
574    enum Notification {
575        #[column(variant = 1)]
576        Send { channel: Channel, message: String },
577        #[column(variant = 2)]
578        Suppress,
579    }
580
581    #[derive(Debug, toasty::Model)]
582    struct Alert {
583        #[key]
584        #[auto]
585        id: uuid::Uuid,
586        notification: Notification,
587    }
588
589    let mut db = test.setup_db(models!(Alert)).await;
590
591    let a1 = Alert::create()
592        .notification(Notification::Send {
593            channel: Channel::Email,
594            message: "hello".to_string(),
595        })
596        .exec(&mut db)
597        .await?;
598
599    let a2 = Alert::create()
600        .notification(Notification::Send {
601            channel: Channel::Sms,
602            message: "world".to_string(),
603        })
604        .exec(&mut db)
605        .await?;
606
607    let a3 = Alert::create()
608        .notification(Notification::Suppress)
609        .exec(&mut db)
610        .await?;
611
612    let found_a1 = Alert::get_by_id(&mut db, &a1.id).await?;
613    assert_eq!(
614        found_a1.notification,
615        Notification::Send {
616            channel: Channel::Email,
617            message: "hello".to_string(),
618        }
619    );
620
621    let found_a2 = Alert::get_by_id(&mut db, &a2.id).await?;
622    assert_eq!(
623        found_a2.notification,
624        Notification::Send {
625            channel: Channel::Sms,
626            message: "world".to_string(),
627        }
628    );
629
630    let found_a3 = Alert::get_by_id(&mut db, &a3.id).await?;
631    assert_eq!(found_a3.notification, Notification::Suppress);
632
633    Ok(())
634}
635
636/// Verifies field indices are assigned globally across multiple data variants.
637/// With two variants having two fields each, indices should be 0, 1, 2, 3.
638#[driver_test]
639pub async fn global_field_indices(test: &mut Test) {
640    #[allow(dead_code)]
641    #[derive(toasty::Embed)]
642    enum Event {
643        #[column(variant = 1)]
644        Login { user_id: String, ip: String },
645        #[column(variant = 2)]
646        Purchase { item_id: String, amount: i64 },
647    }
648
649    #[derive(toasty::Model)]
650    #[allow(dead_code)]
651    struct Container {
652        #[key]
653        id: i64,
654        event: Event,
655    }
656
657    let db = test.setup_db(models!(Container)).await;
658    let schema = db.schema();
659
660    let event = &schema.app.models[&Event::id()];
661    assert_struct!(event, toasty::schema::app::Model::EmbeddedEnum({
662        fields: [
663            { id.index: 0, name.app: Some("user_id") },
664            { id.index: 1, name.app: Some("ip") },
665            { id.index: 2, name.app: Some("item_id") },
666            { id.index: 3, name.app: Some("amount") },
667        ],
668    }));
669}
670
671/// A data-carrying variant named `Create` keeps its `create()` field
672/// accessor on the enum's fields handle: `create!` and `update!` obtain the
673/// construction builder from the enum itself, through `EmbedCreate`, so the
674/// handle carries no method of the same name.
675#[driver_test]
676pub async fn variant_named_create(test: &mut Test) -> Result<()> {
677    #[derive(Debug, PartialEq, toasty::Embed)]
678    enum Action {
679        Create { name: String },
680        Delete,
681    }
682
683    #[derive(Debug, toasty::Model)]
684    #[key(partition = group, local = id)]
685    struct Event {
686        #[auto]
687        id: uuid::Uuid,
688        group: String,
689        action: Action,
690    }
691
692    let mut db = test.setup_db(models!(Event)).await;
693
694    let mut event = toasty::create!(Event {
695        group: "audit",
696        action: Action::Create {
697            name: "alpha".to_string()
698        },
699    })
700    .exec(&mut db)
701    .await?;
702
703    let found = Event::filter(
704        Event::fields().group().eq("audit").and(
705            Event::fields()
706                .action()
707                .create()
708                .matches(|c| c.name().eq("alpha")),
709        ),
710    )
711    .exec(&mut db)
712    .await?;
713    assert_struct!(found, [{ id: == event.id }]);
714
715    toasty::update!(event {
716        action: Action::Create {
717            name: "beta".to_string()
718        },
719    })
720    .exec(&mut db)
721    .await?;
722
723    let reloaded = Event::get_by_group_and_id(&mut db, "audit", &event.id).await?;
724    assert_eq!(
725        reloaded.action,
726        Action::Create {
727            name: "beta".to_string()
728        }
729    );
730
731    Ok(())
732}