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]
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#[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#[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}