Skip to main content

toasty_driver_integration_suite/tests/
embed_enum_optional.rs

1//! `Option<EmbeddedEnum>` model fields.
2//!
3//! A nullable embedded enum reuses its discriminant column as the head: `NULL`
4//! = `None`, a variant value = `Some(variant)`. The data-enum decode `Match`
5//! gets a `null` else branch so a `NULL` (unmatched) discriminant decodes to
6//! `None`; a unit-only enum's discriminant column reads back as `None` directly.
7//!
8//! Variant-only predicates (`.is_email()`) on the optional path are not yet
9//! exposed — that needs the same `Field` path-machinery work deferred for
10//! nested-field access on `Option<struct>`. Whole-value `.eq(Some(Variant))`
11//! covers variant matching here.
12
13use crate::helpers::column;
14use crate::prelude::*;
15
16use toasty_core::stmt::{Expr, Value};
17
18/// The DB schema: nullable discriminant columns (named after the field) plus
19/// nullable variant-field columns for the data enum.
20#[driver_test(scenario(crate::scenarios::account_optional_contact_status))]
21pub async fn option_enum_db_schema(test: &mut Test) {
22    let db = setup(test).await;
23    let schema = db.schema();
24
25    assert_struct!(schema.db.tables, [
26        {
27            name: =~ r"accounts$",
28            columns: [
29                { name: "id" },
30                { name: "contact", nullable: true },
31                { name: "contact_address", nullable: true },
32                { name: "contact_number", nullable: true },
33                { name: "status", nullable: true },
34            ],
35        },
36    ]);
37}
38
39/// Round-trip `Some(variant)` and `None` for both a data enum and a unit enum.
40#[driver_test(scenario(crate::scenarios::account_optional_contact_status))]
41pub async fn option_enum_crud(test: &mut Test) -> Result<()> {
42    let mut db = setup(test).await;
43
44    toasty::create!(Account {
45        id: "full",
46        contact: Some(Contact::Email {
47            address: "a@x.com".to_string(),
48        }),
49        status: Some(Status::Active),
50    })
51    .exec(&mut db)
52    .await?;
53
54    toasty::create!(Account {
55        id: "empty",
56        contact: None,
57        status: None,
58    })
59    .exec(&mut db)
60    .await?;
61
62    let full = Account::get_by_id(&mut db, "full").await?;
63    assert_struct!(full, {
64        contact: Some(== Contact::Email { address: "a@x.com".to_string() }),
65        status: Some(== Status::Active),
66    });
67
68    let empty = Account::get_by_id(&mut db, "empty").await?;
69    assert_none!(empty.contact);
70    assert_none!(empty.status);
71
72    Ok(())
73}
74
75/// `.is_none()` / `.is_some()` on an `Option<enum>` filter the discriminant
76/// column's null-ness.
77#[driver_test(
78    requires(scan),
79    scenario(crate::scenarios::account_optional_contact_status)
80)]
81pub async fn option_enum_filter_presence(test: &mut Test) -> Result<()> {
82    let mut db = setup(test).await;
83
84    toasty::create!(Account {
85        id: "full",
86        contact: Some(Contact::Phone {
87            number: "555".to_string(),
88        }),
89        status: Some(Status::Inactive),
90    })
91    .exec(&mut db)
92    .await?;
93    toasty::create!(Account {
94        id: "empty",
95        contact: None,
96        status: None,
97    })
98    .exec(&mut db)
99    .await?;
100
101    // Data enum.
102    let present = Account::filter(Account::fields().contact().is_some())
103        .exec(&mut db)
104        .await?;
105    assert_struct!(present, [{ id: "full" }]);
106
107    let absent = Account::filter(Account::fields().contact().is_none())
108        .exec(&mut db)
109        .await?;
110    assert_struct!(absent, [{ id: "empty" }]);
111
112    // Unit enum.
113    let status_absent = Account::filter(Account::fields().status().is_none())
114        .exec(&mut db)
115        .await?;
116    assert_struct!(status_absent, [{ id: "empty" }]);
117
118    Ok(())
119}
120
121/// Whole-value `.eq(Some(variant))` matches the equal `Some` row and never
122/// `None`, for both a data variant and a unit variant.
123#[driver_test(
124    requires(scan),
125    scenario(crate::scenarios::account_optional_contact_status)
126)]
127pub async fn option_enum_filter_eq(test: &mut Test) -> Result<()> {
128    let mut db = setup(test).await;
129
130    toasty::create!(Account {
131        id: "alice",
132        contact: Some(Contact::Email {
133            address: "alice@x.com".to_string(),
134        }),
135        status: Some(Status::Active),
136    })
137    .exec(&mut db)
138    .await?;
139    toasty::create!(Account {
140        id: "bob",
141        contact: Some(Contact::Phone {
142            number: "555".to_string(),
143        }),
144        status: Some(Status::Inactive),
145    })
146    .exec(&mut db)
147    .await?;
148    toasty::create!(Account {
149        id: "empty",
150        contact: None,
151        status: None,
152    })
153    .exec(&mut db)
154    .await?;
155
156    let email = Account::filter(Account::fields().contact().eq(Some(Contact::Email {
157        address: "alice@x.com".to_string(),
158    })))
159    .exec(&mut db)
160    .await?;
161    assert_struct!(email, [{ id: "alice" }]);
162
163    let active = Account::filter(Account::fields().status().eq(Some(Status::Active)))
164        .exec(&mut db)
165        .await?;
166    assert_struct!(active, [{ id: "alice" }]);
167
168    Ok(())
169}
170
171/// Updating the whole value in both directions for a data and a unit enum.
172#[driver_test(scenario(crate::scenarios::account_optional_contact_status))]
173pub async fn option_enum_update(test: &mut Test) -> Result<()> {
174    let mut db = setup(test).await;
175
176    toasty::create!(Account {
177        id: "a",
178        contact: Some(Contact::Email {
179            address: "a@x.com".to_string(),
180        }),
181        status: None,
182    })
183    .exec(&mut db)
184    .await?;
185
186    // Some -> None (data enum).
187    Account::filter_by_id("a")
188        .update()
189        .contact(None)
190        .exec(&mut db)
191        .await?;
192    assert_none!(Account::get_by_id(&mut db, "a").await?.contact);
193
194    // None -> Some (unit enum).
195    Account::filter_by_id("a")
196        .update()
197        .status(Some(Status::Active))
198        .exec(&mut db)
199        .await?;
200    assert_struct!(
201        Account::get_by_id(&mut db, "a").await?.status,
202        Some(== Status::Active)
203    );
204
205    Ok(())
206}
207
208/// Driver-op coverage for create. A `Some(variant)` writes the variant
209/// discriminant to the (shared) discriminant column and the variant's fields to
210/// their columns (other variants' columns `NULL`); a `None` writes `NULL` to the
211/// discriminant *and* every variant column.
212#[driver_test(scenario(crate::scenarios::account_optional_contact_status))]
213pub async fn option_enum_create_ops(test: &mut Test) -> Result<()> {
214    let mut db = setup(test).await;
215    let contact = column(&db, "accounts", "contact").index;
216    let address = column(&db, "accounts", "contact_address").index;
217    let number = column(&db, "accounts", "contact_number").index;
218    let status = column(&db, "accounts", "status").index;
219    test.log().clear();
220
221    // Some: discriminant is the variant value (Email = 1, Active = 1); only the
222    // active variant's column is set.
223    toasty::create!(Account {
224        id: "a",
225        contact: Some(Contact::Email {
226            address: "a@x.com".to_string(),
227        }),
228        status: Some(Status::Active),
229    })
230    .exec(&mut db)
231    .await?;
232    let row = pop_insert(test);
233    assert_eq!(row[&contact], Value::from(1i64));
234    assert_eq!(row[&address], Value::from("a@x.com"));
235    assert_eq!(row[&number], Value::Null);
236    assert_eq!(row[&status], Value::from(1i64));
237
238    // None: discriminant and every variant column are NULL.
239    toasty::create!(Account {
240        id: "b",
241        contact: None,
242        status: None,
243    })
244    .exec(&mut db)
245    .await?;
246    let row = pop_insert(test);
247    assert_eq!(row[&contact], Value::Null);
248    assert_eq!(row[&address], Value::Null);
249    assert_eq!(row[&number], Value::Null);
250    assert_eq!(row[&status], Value::Null);
251
252    Ok(())
253}
254
255/// Driver-op coverage for filters. `.is_none()` / `.is_some()` emit a predicate
256/// on the single discriminant column (`IS NULL` / `NOT (.. IS NULL)`), never a
257/// check distributed across the variant columns.
258#[driver_test(
259    requires(scan),
260    scenario(crate::scenarios::account_optional_contact_status)
261)]
262pub async fn option_enum_filter_ops(test: &mut Test) -> Result<()> {
263    let mut db = setup(test).await;
264    let contact = column(&db, "accounts", "contact").index;
265    test.log().clear();
266
267    // `.is_none()` → `contact IS NULL`.
268    let _ = Account::filter(Account::fields().contact().is_none())
269        .exec(&mut db)
270        .await?;
271    assert_struct!(pop_filter(test), Expr::IsNull({
272        expr.as_expr_column_unwrap().column: == contact,
273    }));
274
275    // `.is_some()` → `NOT (contact IS NULL)`.
276    let _ = Account::filter(Account::fields().contact().is_some())
277        .exec(&mut db)
278        .await?;
279    let Expr::Not(not) = pop_filter(test) else {
280        panic!("expected NOT");
281    };
282    assert_struct!(*not.expr, Expr::IsNull({
283        expr.as_expr_column_unwrap().column: == contact,
284    }));
285
286    Ok(())
287}
288
289/// Driver-op coverage for updates. Setting `Some(variant)` assigns the variant
290/// discriminant + that variant's column (others `NULL`); setting `None` assigns
291/// `NULL` to the discriminant and every variant column.
292#[driver_test(scenario(crate::scenarios::account_optional_contact_status))]
293pub async fn option_enum_update_ops(test: &mut Test) -> Result<()> {
294    let mut db = setup(test).await;
295    let contact = column(&db, "accounts", "contact").index;
296    let address = column(&db, "accounts", "contact_address").index;
297    let number = column(&db, "accounts", "contact_number").index;
298
299    toasty::create!(Account {
300        id: "a",
301        contact: None,
302        status: None,
303    })
304    .exec(&mut db)
305    .await?;
306
307    // None -> Some(Phone): discriminant = 2, number set, address NULL.
308    test.log().clear();
309    Account::filter_by_id("a")
310        .update()
311        .contact(Some(Contact::Phone {
312            number: "555".to_string(),
313        }))
314        .exec(&mut db)
315        .await?;
316    let set = pop_update(test);
317    assert_eq!(set[&contact], Value::from(2i64));
318    assert_eq!(set[&number], Value::from("555"));
319    assert_eq!(set[&address], Value::Null);
320
321    // Some -> None: discriminant and every variant column NULL.
322    test.log().clear();
323    Account::filter_by_id("a")
324        .update()
325        .contact(None)
326        .exec(&mut db)
327        .await?;
328    let set = pop_update(test);
329    assert_eq!(set[&contact], Value::Null);
330    assert_eq!(set[&address], Value::Null);
331    assert_eq!(set[&number], Value::Null);
332
333    Ok(())
334}