1use crate::prelude::*;
2
3#[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#[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#[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 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#[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#[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 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 alice.delete().exec(&mut db).await?;
210 bob.delete().exec(&mut db).await?;
211 Ok(())
212}
213
214#[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#[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 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 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#[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 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#[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#[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#[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#[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#[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#[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}