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 jiff::Timestamp fields inside data-carrying enum variants round-trip correctly.
399/// Also covers a mixed enum (one unit variant, one data variant) to verify null handling.
400#[driver_test]
401pub async fn data_variant_with_jiff_timestamp(test: &mut Test) -> Result<()> {
402    #[derive(Debug, PartialEq, toasty::Embed)]
403    enum EventTime {
404        #[column(variant = 1)]
405        Scheduled { at: jiff::Timestamp },
406        #[column(variant = 2)]
407        Unscheduled,
408    }
409
410    #[derive(Debug, toasty::Model)]
411    struct Event {
412        #[key]
413        #[auto]
414        id: uuid::Uuid,
415        name: String,
416        time: EventTime,
417    }
418
419    let mut db = test.setup_db(models!(Event)).await;
420
421    let ts = jiff::Timestamp::from_second(1_700_000_000).unwrap();
422
423    let scheduled = Event::create()
424        .name("launch")
425        .time(EventTime::Scheduled { at: ts })
426        .exec(&mut db)
427        .await?;
428
429    let unscheduled = Event::create()
430        .name("tbd")
431        .time(EventTime::Unscheduled)
432        .exec(&mut db)
433        .await?;
434
435    let found_scheduled = Event::get_by_id(&mut db, &scheduled.id).await?;
436    assert_eq!(found_scheduled.time, EventTime::Scheduled { at: ts });
437
438    let found_unscheduled = Event::get_by_id(&mut db, &unscheduled.id).await?;
439    assert_eq!(found_unscheduled.time, EventTime::Unscheduled);
440
441    Ok(())
442}
443
444#[driver_test]
445pub async fn struct_in_data_variant(test: &mut Test) -> Result<()> {
446    #[derive(Debug, PartialEq, toasty::Embed)]
447    struct Address {
448        street: String,
449        city: String,
450    }
451
452    #[derive(Debug, PartialEq, toasty::Embed)]
453    enum Destination {
454        #[column(variant = 1)]
455        Digital { email: String },
456        #[column(variant = 2)]
457        Physical { address: Address },
458    }
459
460    #[derive(Debug, toasty::Model)]
461    struct Shipment {
462        #[key]
463        #[auto]
464        id: uuid::Uuid,
465        destination: Destination,
466    }
467
468    let mut db = test.setup_db(models!(Shipment)).await;
469
470    let digital = Shipment::create()
471        .destination(Destination::Digital {
472            email: "user@example.com".to_string(),
473        })
474        .exec(&mut db)
475        .await?;
476
477    let physical = Shipment::create()
478        .destination(Destination::Physical {
479            address: Address {
480                street: "123 Main St".to_string(),
481                city: "Seattle".to_string(),
482            },
483        })
484        .exec(&mut db)
485        .await?;
486
487    let found_digital = Shipment::get_by_id(&mut db, &digital.id).await?;
488    assert_eq!(
489        found_digital.destination,
490        Destination::Digital {
491            email: "user@example.com".to_string()
492        }
493    );
494
495    let found_physical = Shipment::get_by_id(&mut db, &physical.id).await?;
496    assert_eq!(
497        found_physical.destination,
498        Destination::Physical {
499            address: Address {
500                street: "123 Main St".to_string(),
501                city: "Seattle".to_string(),
502            },
503        }
504    );
505
506    Ok(())
507}
508
509/// Roundtrip test for an enum embedded inside a variant field of another enum (enum-in-enum).
510/// The inner enum is unit-only; the outer has one data variant and one unit variant.
511#[driver_test]
512pub async fn enum_in_enum_roundtrip(test: &mut Test) -> Result<()> {
513    #[derive(Debug, PartialEq, toasty::Embed)]
514    enum Channel {
515        #[column(variant = 1)]
516        Email,
517        #[column(variant = 2)]
518        Sms,
519    }
520
521    #[derive(Debug, PartialEq, toasty::Embed)]
522    enum Notification {
523        #[column(variant = 1)]
524        Send { channel: Channel, message: String },
525        #[column(variant = 2)]
526        Suppress,
527    }
528
529    #[derive(Debug, toasty::Model)]
530    struct Alert {
531        #[key]
532        #[auto]
533        id: uuid::Uuid,
534        notification: Notification,
535    }
536
537    let mut db = test.setup_db(models!(Alert)).await;
538
539    let a1 = Alert::create()
540        .notification(Notification::Send {
541            channel: Channel::Email,
542            message: "hello".to_string(),
543        })
544        .exec(&mut db)
545        .await?;
546
547    let a2 = Alert::create()
548        .notification(Notification::Send {
549            channel: Channel::Sms,
550            message: "world".to_string(),
551        })
552        .exec(&mut db)
553        .await?;
554
555    let a3 = Alert::create()
556        .notification(Notification::Suppress)
557        .exec(&mut db)
558        .await?;
559
560    let found_a1 = Alert::get_by_id(&mut db, &a1.id).await?;
561    assert_eq!(
562        found_a1.notification,
563        Notification::Send {
564            channel: Channel::Email,
565            message: "hello".to_string(),
566        }
567    );
568
569    let found_a2 = Alert::get_by_id(&mut db, &a2.id).await?;
570    assert_eq!(
571        found_a2.notification,
572        Notification::Send {
573            channel: Channel::Sms,
574            message: "world".to_string(),
575        }
576    );
577
578    let found_a3 = Alert::get_by_id(&mut db, &a3.id).await?;
579    assert_eq!(found_a3.notification, Notification::Suppress);
580
581    Ok(())
582}
583
584/// Verifies field indices are assigned globally across multiple data variants.
585/// With two variants having two fields each, indices should be 0, 1, 2, 3.
586#[driver_test]
587pub async fn global_field_indices(test: &mut Test) {
588    #[allow(dead_code)]
589    #[derive(toasty::Embed)]
590    enum Event {
591        #[column(variant = 1)]
592        Login { user_id: String, ip: String },
593        #[column(variant = 2)]
594        Purchase { item_id: String, amount: i64 },
595    }
596
597    #[derive(toasty::Model)]
598    #[allow(dead_code)]
599    struct Container {
600        #[key]
601        id: i64,
602        event: Event,
603    }
604
605    let db = test.setup_db(models!(Container)).await;
606    let schema = db.schema();
607
608    let event = &schema.app.models[&Event::id()];
609    assert_struct!(event, toasty::schema::app::Model::EmbeddedEnum({
610        fields: [
611            { id.index: 0, name.app: Some("user_id") },
612            { id.index: 1, name.app: Some("ip") },
613            { id.index: 2, name.app: Some("item_id") },
614            { id.index: 3, name.app: Some("amount") },
615        ],
616    }));
617}