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            {
20                name.upper_camel_case(): "Phone",
21                discriminant: toasty_core::stmt::Value::I64(2),
22                ..
23            },
24        ],
25        fields: [
26            { id.index: 0, name.app: Some("address") },
27            { id.index: 1, name.app: Some("number") },
28        ],
29    }));
30}
31
32/// Verifies that a mixed enum (some unit variants, some data variants) registers
33/// correctly: unit variants have empty `fields`, data variants have their fields
34/// with indices assigned starting from 0 and continuing globally across variants.
35#[driver_test(scenario(crate::scenarios::task_with_status))]
36pub async fn mixed_enum_schema(t: &mut Test) {
37    let db = setup(t).await;
38    let schema = db.schema();
39
40    let status = &schema.app.models[&Status::id()];
41    assert_struct!(status, toasty::schema::app::Model::EmbeddedEnum({
42        variants: [
43            {
44                name.upper_camel_case(): "Pending",
45                discriminant: toasty_core::stmt::Value::I64(1),
46                ..
47            },
48            {
49                name.upper_camel_case(): "Failed",
50                discriminant: toasty_core::stmt::Value::I64(2),
51                ..
52            },
53            {
54                name.upper_camel_case(): "Done",
55                discriminant: toasty_core::stmt::Value::I64(3),
56                ..
57            },
58        ],
59        fields: [
60            { id.index: 0, name.app: Some("reason") },
61        ],
62    }));
63}
64
65/// Verifies DB columns for a data-carrying enum: discriminant column + one nullable
66/// column per variant field, named `{disc_col}_{field_name}`.
67#[driver_test]
68pub async fn data_carrying_enum_db_schema(test: &mut Test) {
69    #[allow(dead_code)]
70    #[derive(toasty::Embed)]
71    enum ContactInfo {
72        #[column(variant = 1)]
73        Email { address: String },
74        #[column(variant = 2)]
75        Phone { number: String },
76    }
77
78    #[derive(toasty::Model)]
79    struct User {
80        #[key]
81        id: String,
82        #[allow(dead_code)]
83        contact: ContactInfo,
84    }
85
86    let db = test.setup_db(models!(User)).await;
87    let schema = db.schema();
88
89    // The DB table has disc col + one col per variant field (2 variants × 1 field each).
90    assert_struct!(schema.db.tables, [
91        {
92            name: =~ r"users$",
93            columns: [
94                { name: "id" },
95                { name: "contact", nullable: false },
96                { name: "contact_address", nullable: true },
97                { name: "contact_number", nullable: true },
98            ],
99        },
100    ]);
101}
102
103/// End-to-end CRUD test for a data-carrying enum (all variants have fields).
104/// Creates records with different variants, reads them back, and verifies roundtrip.
105#[driver_test(scenario(crate::scenarios::user_contact_info))]
106pub async fn data_variant_roundtrip(test: &mut Test) -> Result<()> {
107    let mut db = setup(test).await;
108
109    let alice = User::create()
110        .name("Alice")
111        .contact(ContactInfo::Email {
112            address: "alice@example.com".to_string(),
113        })
114        .exec(&mut db)
115        .await?;
116
117    let bob = User::create()
118        .name("Bob")
119        .contact(ContactInfo::Phone {
120            number: "555-1234".to_string(),
121        })
122        .exec(&mut db)
123        .await?;
124
125    // Read back and check values are reconstructed correctly.
126    let found_alice = User::get_by_id(&mut db, &alice.id).await?;
127    assert_eq!(
128        found_alice.contact,
129        ContactInfo::Email {
130            address: "alice@example.com".to_string()
131        }
132    );
133
134    let found_bob = User::get_by_id(&mut db, &bob.id).await?;
135    assert_eq!(
136        found_bob.contact,
137        ContactInfo::Phone {
138            number: "555-1234".to_string()
139        }
140    );
141
142    // Clean up.
143    alice.delete().exec(&mut db).await?;
144    bob.delete().exec(&mut db).await?;
145    Ok(())
146}
147
148/// End-to-end CRUD test for a mixed enum (unit variants and data variants).
149/// Verifies that both kinds round-trip correctly through the DB.
150#[driver_test(scenario(crate::scenarios::task_with_status))]
151pub async fn mixed_enum_roundtrip(t: &mut Test) -> Result<()> {
152    let mut db = setup(t).await;
153
154    let pending = Task::create()
155        .title("Pending task")
156        .status(Status::Pending)
157        .exec(&mut db)
158        .await?;
159
160    let failed = Task::create()
161        .title("Failed task")
162        .status(Status::Failed {
163            reason: "out of memory".to_string(),
164        })
165        .exec(&mut db)
166        .await?;
167
168    let done = Task::create()
169        .title("Done task")
170        .status(Status::Done)
171        .exec(&mut db)
172        .await?;
173
174    let found_pending = Task::get_by_id(&mut db, &pending.id).await?;
175    assert_eq!(found_pending.status, Status::Pending);
176
177    let found_failed = Task::get_by_id(&mut db, &failed.id).await?;
178    assert_eq!(
179        found_failed.status,
180        Status::Failed {
181            reason: "out of memory".to_string()
182        }
183    );
184
185    let found_done = Task::get_by_id(&mut db, &done.id).await?;
186    assert_eq!(found_done.status, Status::Done);
187
188    Ok(())
189}
190
191/// Updating a mixed enum field from a data-carrying variant to a unit variant
192/// (and back) round-trips correctly. Regression test for #1068: the unit
193/// variant's value record is narrower than the data variant's data column
194/// expects, which used to panic while lowering the update.
195#[driver_test(scenario(crate::scenarios::task_with_status))]
196pub async fn mixed_enum_update_data_to_unit_variant(t: &mut Test) -> Result<()> {
197    let mut db = setup(t).await;
198
199    let mut task = toasty::create!(Task {
200        title: "task",
201        status: Status::Failed {
202            reason: "boom".to_string(),
203        },
204    })
205    .exec(&mut db)
206    .await?;
207
208    // Data variant -> unit variant (the reported panic).
209    task.update().status(Status::Pending).exec(&mut db).await?;
210    assert_eq!(
211        Task::get_by_id(&mut db, &task.id).await?.status,
212        Status::Pending
213    );
214
215    // Unit variant -> data variant, to confirm the reverse still works.
216    task.update()
217        .status(Status::Failed {
218            reason: "again".to_string(),
219        })
220        .exec(&mut db)
221        .await?;
222    assert_eq!(
223        Task::get_by_id(&mut db, &task.id).await?.status,
224        Status::Failed {
225            reason: "again".to_string()
226        }
227    );
228
229    Ok(())
230}
231
232/// Updating a data-carrying enum between variants with *different field counts*
233/// round-trips. The narrower variant's value record is shorter than the wider
234/// variant's data columns expect, exercising the same out-of-bounds projection
235/// path as #1068 without any unit variant involved.
236#[driver_test]
237pub async fn enum_update_between_variants_of_different_width(test: &mut Test) -> Result<()> {
238    #[derive(Debug, PartialEq, toasty::Embed)]
239    enum Event {
240        #[column(variant = 1)]
241        Login { user: String },
242        #[column(variant = 2)]
243        Purchase { item: String, amount: i64 },
244    }
245
246    #[derive(Debug, toasty::Model)]
247    struct Log {
248        #[key]
249        #[auto]
250        id: uuid::Uuid,
251        event: Event,
252    }
253
254    let mut db = test.setup_db(models!(Log)).await;
255
256    let mut log = toasty::create!(Log {
257        event: Event::Purchase {
258            item: "book".to_string(),
259            amount: 42,
260        },
261    })
262    .exec(&mut db)
263    .await?;
264
265    // Wider variant -> narrower variant.
266    log.update()
267        .event(Event::Login {
268            user: "alice".to_string(),
269        })
270        .exec(&mut db)
271        .await?;
272    assert_eq!(
273        Log::get_by_id(&mut db, &log.id).await?.event,
274        Event::Login {
275            user: "alice".to_string()
276        }
277    );
278
279    Ok(())
280}
281
282/// Tests that UUID fields inside data-carrying enum variants round-trip correctly.
283/// UUID is a non-trivial primitive that requires type casting on some databases.
284#[driver_test]
285pub async fn data_variant_with_uuid_field(test: &mut Test) -> Result<()> {
286    #[derive(Debug, PartialEq, toasty::Embed)]
287    enum OrderRef {
288        #[column(variant = 1)]
289        Internal { id: uuid::Uuid },
290        #[column(variant = 2)]
291        External { code: String },
292    }
293
294    #[derive(Debug, toasty::Model)]
295    struct Order {
296        #[key]
297        #[auto]
298        id: uuid::Uuid,
299        order_ref: OrderRef,
300    }
301
302    let mut db = test.setup_db(models!(Order)).await;
303
304    let internal_id = uuid::Uuid::new_v4();
305
306    let o1 = Order::create()
307        .order_ref(OrderRef::Internal { id: internal_id })
308        .exec(&mut db)
309        .await?;
310
311    let o2 = Order::create()
312        .order_ref(OrderRef::External {
313            code: "EXT-001".to_string(),
314        })
315        .exec(&mut db)
316        .await?;
317
318    let found_o1 = Order::get_by_id(&mut db, &o1.id).await?;
319    assert_eq!(found_o1.order_ref, OrderRef::Internal { id: internal_id });
320
321    let found_o2 = Order::get_by_id(&mut db, &o2.id).await?;
322    assert_eq!(
323        found_o2.order_ref,
324        OrderRef::External {
325            code: "EXT-001".to_string()
326        }
327    );
328
329    Ok(())
330}
331
332/// Tests that jiff::Timestamp fields inside data-carrying enum variants round-trip correctly.
333/// Also covers a mixed enum (one unit variant, one data variant) to verify null handling.
334#[driver_test]
335pub async fn data_variant_with_jiff_timestamp(test: &mut Test) -> Result<()> {
336    #[derive(Debug, PartialEq, toasty::Embed)]
337    enum EventTime {
338        #[column(variant = 1)]
339        Scheduled { at: jiff::Timestamp },
340        #[column(variant = 2)]
341        Unscheduled,
342    }
343
344    #[derive(Debug, toasty::Model)]
345    struct Event {
346        #[key]
347        #[auto]
348        id: uuid::Uuid,
349        name: String,
350        time: EventTime,
351    }
352
353    let mut db = test.setup_db(models!(Event)).await;
354
355    let ts = jiff::Timestamp::from_second(1_700_000_000).unwrap();
356
357    let scheduled = Event::create()
358        .name("launch")
359        .time(EventTime::Scheduled { at: ts })
360        .exec(&mut db)
361        .await?;
362
363    let unscheduled = Event::create()
364        .name("tbd")
365        .time(EventTime::Unscheduled)
366        .exec(&mut db)
367        .await?;
368
369    let found_scheduled = Event::get_by_id(&mut db, &scheduled.id).await?;
370    assert_eq!(found_scheduled.time, EventTime::Scheduled { at: ts });
371
372    let found_unscheduled = Event::get_by_id(&mut db, &unscheduled.id).await?;
373    assert_eq!(found_unscheduled.time, EventTime::Unscheduled);
374
375    Ok(())
376}
377
378#[driver_test]
379pub async fn struct_in_data_variant(test: &mut Test) -> Result<()> {
380    #[derive(Debug, PartialEq, toasty::Embed)]
381    struct Address {
382        street: String,
383        city: String,
384    }
385
386    #[derive(Debug, PartialEq, toasty::Embed)]
387    enum Destination {
388        #[column(variant = 1)]
389        Digital { email: String },
390        #[column(variant = 2)]
391        Physical { address: Address },
392    }
393
394    #[derive(Debug, toasty::Model)]
395    struct Shipment {
396        #[key]
397        #[auto]
398        id: uuid::Uuid,
399        destination: Destination,
400    }
401
402    let mut db = test.setup_db(models!(Shipment)).await;
403
404    let digital = Shipment::create()
405        .destination(Destination::Digital {
406            email: "user@example.com".to_string(),
407        })
408        .exec(&mut db)
409        .await?;
410
411    let physical = Shipment::create()
412        .destination(Destination::Physical {
413            address: Address {
414                street: "123 Main St".to_string(),
415                city: "Seattle".to_string(),
416            },
417        })
418        .exec(&mut db)
419        .await?;
420
421    let found_digital = Shipment::get_by_id(&mut db, &digital.id).await?;
422    assert_eq!(
423        found_digital.destination,
424        Destination::Digital {
425            email: "user@example.com".to_string()
426        }
427    );
428
429    let found_physical = Shipment::get_by_id(&mut db, &physical.id).await?;
430    assert_eq!(
431        found_physical.destination,
432        Destination::Physical {
433            address: Address {
434                street: "123 Main St".to_string(),
435                city: "Seattle".to_string(),
436            },
437        }
438    );
439
440    Ok(())
441}
442
443/// Roundtrip test for an enum embedded inside a variant field of another enum (enum-in-enum).
444/// The inner enum is unit-only; the outer has one data variant and one unit variant.
445#[driver_test]
446pub async fn enum_in_enum_roundtrip(test: &mut Test) -> Result<()> {
447    #[derive(Debug, PartialEq, toasty::Embed)]
448    enum Channel {
449        #[column(variant = 1)]
450        Email,
451        #[column(variant = 2)]
452        Sms,
453    }
454
455    #[derive(Debug, PartialEq, toasty::Embed)]
456    enum Notification {
457        #[column(variant = 1)]
458        Send { channel: Channel, message: String },
459        #[column(variant = 2)]
460        Suppress,
461    }
462
463    #[derive(Debug, toasty::Model)]
464    struct Alert {
465        #[key]
466        #[auto]
467        id: uuid::Uuid,
468        notification: Notification,
469    }
470
471    let mut db = test.setup_db(models!(Alert)).await;
472
473    let a1 = Alert::create()
474        .notification(Notification::Send {
475            channel: Channel::Email,
476            message: "hello".to_string(),
477        })
478        .exec(&mut db)
479        .await?;
480
481    let a2 = Alert::create()
482        .notification(Notification::Send {
483            channel: Channel::Sms,
484            message: "world".to_string(),
485        })
486        .exec(&mut db)
487        .await?;
488
489    let a3 = Alert::create()
490        .notification(Notification::Suppress)
491        .exec(&mut db)
492        .await?;
493
494    let found_a1 = Alert::get_by_id(&mut db, &a1.id).await?;
495    assert_eq!(
496        found_a1.notification,
497        Notification::Send {
498            channel: Channel::Email,
499            message: "hello".to_string(),
500        }
501    );
502
503    let found_a2 = Alert::get_by_id(&mut db, &a2.id).await?;
504    assert_eq!(
505        found_a2.notification,
506        Notification::Send {
507            channel: Channel::Sms,
508            message: "world".to_string(),
509        }
510    );
511
512    let found_a3 = Alert::get_by_id(&mut db, &a3.id).await?;
513    assert_eq!(found_a3.notification, Notification::Suppress);
514
515    Ok(())
516}
517
518/// Verifies field indices are assigned globally across multiple data variants.
519/// With two variants having two fields each, indices should be 0, 1, 2, 3.
520#[driver_test]
521pub async fn global_field_indices(test: &mut Test) {
522    #[allow(dead_code)]
523    #[derive(toasty::Embed)]
524    enum Event {
525        #[column(variant = 1)]
526        Login { user_id: String, ip: String },
527        #[column(variant = 2)]
528        Purchase { item_id: String, amount: i64 },
529    }
530
531    #[derive(toasty::Model)]
532    #[allow(dead_code)]
533    struct Container {
534        #[key]
535        id: i64,
536        event: Event,
537    }
538
539    let db = test.setup_db(models!(Container)).await;
540    let schema = db.schema();
541
542    let event = &schema.app.models[&Event::id()];
543    assert_struct!(event, toasty::schema::app::Model::EmbeddedEnum({
544        fields: [
545            { id.index: 0, name.app: Some("user_id") },
546            { id.index: 1, name.app: Some("ip") },
547            { id.index: 2, name.app: Some("item_id") },
548            { id.index: 3, name.app: Some("amount") },
549        ],
550    }));
551}