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
69    let empty = Account::get_by_id(&mut db, "empty").await?;
70    assert_none!(empty.contact);
71    assert_none!(empty.status);
72
73    Ok(())
74}
75
76/// `.is_none()` / `.is_some()` on an `Option<enum>` filter the discriminant
77/// column's null-ness.
78#[driver_test(
79    requires(scan),
80    scenario(crate::scenarios::account_optional_contact_status)
81)]
82pub async fn option_enum_filter_presence(test: &mut Test) -> Result<()> {
83    let mut db = setup(test).await;
84
85    toasty::create!(Account {
86        id: "full",
87        contact: Some(Contact::Phone {
88            number: "555".to_string(),
89        }),
90        status: Some(Status::Inactive),
91    })
92    .exec(&mut db)
93    .await?;
94    toasty::create!(Account {
95        id: "empty",
96        contact: None,
97        status: None,
98    })
99    .exec(&mut db)
100    .await?;
101
102    // Data enum.
103    let present = Account::filter(Account::fields().contact().is_some())
104        .exec(&mut db)
105        .await?;
106    assert_struct!(present, [_ { id: "full", .. }]);
107
108    let absent = Account::filter(Account::fields().contact().is_none())
109        .exec(&mut db)
110        .await?;
111    assert_struct!(absent, [_ { id: "empty", .. }]);
112
113    // Unit enum.
114    let status_absent = Account::filter(Account::fields().status().is_none())
115        .exec(&mut db)
116        .await?;
117    assert_struct!(status_absent, [_ { id: "empty", .. }]);
118
119    Ok(())
120}
121
122/// Whole-value `.eq(Some(variant))` matches the equal `Some` row and never
123/// `None`, for both a data variant and a unit variant.
124#[driver_test(
125    requires(scan),
126    scenario(crate::scenarios::account_optional_contact_status)
127)]
128pub async fn option_enum_filter_eq(test: &mut Test) -> Result<()> {
129    let mut db = setup(test).await;
130
131    toasty::create!(Account {
132        id: "alice",
133        contact: Some(Contact::Email {
134            address: "alice@x.com".to_string(),
135        }),
136        status: Some(Status::Active),
137    })
138    .exec(&mut db)
139    .await?;
140    toasty::create!(Account {
141        id: "bob",
142        contact: Some(Contact::Phone {
143            number: "555".to_string(),
144        }),
145        status: Some(Status::Inactive),
146    })
147    .exec(&mut db)
148    .await?;
149    toasty::create!(Account {
150        id: "empty",
151        contact: None,
152        status: None,
153    })
154    .exec(&mut db)
155    .await?;
156
157    let email = Account::filter(Account::fields().contact().eq(Some(Contact::Email {
158        address: "alice@x.com".to_string(),
159    })))
160    .exec(&mut db)
161    .await?;
162    assert_struct!(email, [_ { id: "alice", .. }]);
163
164    let active = Account::filter(Account::fields().status().eq(Some(Status::Active)))
165        .exec(&mut db)
166        .await?;
167    assert_struct!(active, [_ { id: "alice", .. }]);
168
169    Ok(())
170}
171
172/// Updating the whole value in both directions for a data and a unit enum.
173#[driver_test(scenario(crate::scenarios::account_optional_contact_status))]
174pub async fn option_enum_update(test: &mut Test) -> Result<()> {
175    let mut db = setup(test).await;
176
177    toasty::create!(Account {
178        id: "a",
179        contact: Some(Contact::Email {
180            address: "a@x.com".to_string(),
181        }),
182        status: None,
183    })
184    .exec(&mut db)
185    .await?;
186
187    // Some -> None (data enum).
188    Account::filter_by_id("a")
189        .update()
190        .contact(None)
191        .exec(&mut db)
192        .await?;
193    assert_none!(Account::get_by_id(&mut db, "a").await?.contact);
194
195    // None -> Some (unit enum).
196    Account::filter_by_id("a")
197        .update()
198        .status(Some(Status::Active))
199        .exec(&mut db)
200        .await?;
201    assert_struct!(
202        Account::get_by_id(&mut db, "a").await?.status,
203        Some(== Status::Active)
204    );
205
206    Ok(())
207}
208
209/// Driver-op coverage for create. A `Some(variant)` writes the variant
210/// discriminant to the (shared) discriminant column and the variant's fields to
211/// their columns (other variants' columns `NULL`); a `None` writes `NULL` to the
212/// discriminant *and* every variant column.
213#[driver_test(scenario(crate::scenarios::account_optional_contact_status))]
214pub async fn option_enum_create_ops(test: &mut Test) -> Result<()> {
215    let mut db = setup(test).await;
216    let contact = column(&db, "accounts", "contact").index;
217    let address = column(&db, "accounts", "contact_address").index;
218    let number = column(&db, "accounts", "contact_number").index;
219    let status = column(&db, "accounts", "status").index;
220    test.log().clear();
221
222    // Some: discriminant is the variant value (Email = 1, Active = 1); only the
223    // active variant's column is set.
224    toasty::create!(Account {
225        id: "a",
226        contact: Some(Contact::Email {
227            address: "a@x.com".to_string(),
228        }),
229        status: Some(Status::Active),
230    })
231    .exec(&mut db)
232    .await?;
233    let row = pop_insert(test);
234    assert_eq!(row[&contact], Value::from(1i64));
235    assert_eq!(row[&address], Value::from("a@x.com"));
236    assert_eq!(row[&number], Value::Null);
237    assert_eq!(row[&status], Value::from(1i64));
238
239    // None: discriminant and every variant column are NULL.
240    toasty::create!(Account {
241        id: "b",
242        contact: None,
243        status: None,
244    })
245    .exec(&mut db)
246    .await?;
247    let row = pop_insert(test);
248    assert_eq!(row[&contact], Value::Null);
249    assert_eq!(row[&address], Value::Null);
250    assert_eq!(row[&number], Value::Null);
251    assert_eq!(row[&status], Value::Null);
252
253    Ok(())
254}
255
256/// Driver-op coverage for filters. `.is_none()` / `.is_some()` emit a predicate
257/// on the single discriminant column (`IS NULL` / `NOT (.. IS NULL)`), never a
258/// check distributed across the variant columns.
259#[driver_test(
260    requires(scan),
261    scenario(crate::scenarios::account_optional_contact_status)
262)]
263pub async fn option_enum_filter_ops(test: &mut Test) -> Result<()> {
264    let mut db = setup(test).await;
265    let contact = column(&db, "accounts", "contact").index;
266    test.log().clear();
267
268    // `.is_none()` → `contact IS NULL`.
269    let _ = Account::filter(Account::fields().contact().is_none())
270        .exec(&mut db)
271        .await?;
272    assert_struct!(pop_filter(test), Expr::IsNull({
273        expr.as_expr_column_unwrap().column: == contact,
274    }));
275
276    // `.is_some()` → `NOT (contact IS NULL)`.
277    let _ = Account::filter(Account::fields().contact().is_some())
278        .exec(&mut db)
279        .await?;
280    let Expr::Not(not) = pop_filter(test) else {
281        panic!("expected NOT");
282    };
283    assert_struct!(*not.expr, Expr::IsNull({
284        expr.as_expr_column_unwrap().column: == contact,
285    }));
286
287    Ok(())
288}
289
290/// Driver-op coverage for updates. Setting `Some(variant)` assigns the variant
291/// discriminant + that variant's column (others `NULL`); setting `None` assigns
292/// `NULL` to the discriminant and every variant column.
293#[driver_test(scenario(crate::scenarios::account_optional_contact_status))]
294pub async fn option_enum_update_ops(test: &mut Test) -> Result<()> {
295    let mut db = setup(test).await;
296    let contact = column(&db, "accounts", "contact").index;
297    let address = column(&db, "accounts", "contact_address").index;
298    let number = column(&db, "accounts", "contact_number").index;
299
300    toasty::create!(Account {
301        id: "a",
302        contact: None,
303        status: None,
304    })
305    .exec(&mut db)
306    .await?;
307
308    // None -> Some(Phone): discriminant = 2, number set, address NULL.
309    test.log().clear();
310    Account::filter_by_id("a")
311        .update()
312        .contact(Some(Contact::Phone {
313            number: "555".to_string(),
314        }))
315        .exec(&mut db)
316        .await?;
317    let set = pop_update(test);
318    assert_eq!(set[&contact], Value::from(2i64));
319    assert_eq!(set[&number], Value::from("555"));
320    assert_eq!(set[&address], Value::Null);
321
322    // Some -> None: discriminant and every variant column NULL.
323    test.log().clear();
324    Account::filter_by_id("a")
325        .update()
326        .contact(None)
327        .exec(&mut db)
328        .await?;
329    let set = pop_update(test);
330    assert_eq!(set[&contact], Value::Null);
331    assert_eq!(set[&address], Value::Null);
332    assert_eq!(set[&number], Value::Null);
333
334    Ok(())
335}