Skip to main content

toasty_driver_integration_suite/tests/
embed_struct_optional.rs

1//! `Option<StructEmbed>` model fields.
2//!
3//! A nullable embedded struct stores a nullable `bool` presence column
4//! (`NULL` = `None`, `true` = `Some`) plus its flattened leaf columns forced
5//! nullable. `None` is `NULL` in the presence column, symmetric with
6//! `Option<scalar>` and an embedded enum's discriminant. This disambiguates
7//! `None` from a `Some` whose fields happen to all be empty.
8//!
9//! A single-column embed (a newtype `Option<Code>` where `Code(String)`) is
10//! the exception: its one flattened leaf already collapses to the field name,
11//! so it reuses that leaf as the head column (`NULL` = `None`) instead of a
12//! dedicated presence column that would collide with it — exactly like
13//! `Option<scalar>`.
14
15use crate::helpers::column;
16use crate::prelude::*;
17
18use toasty_core::stmt::{Expr, Value};
19
20/// The DB schema: a nullable `bool` presence column named after the field,
21/// plus one nullable column per flattened leaf field.
22#[driver_test(scenario(crate::scenarios::document_optional_metadata))]
23pub async fn option_embed_db_schema(test: &mut Test) {
24    let db = setup(test).await;
25    let schema = db.schema();
26
27    assert_struct!(schema.db.tables, [
28        {
29            name: =~ r"documents$",
30            columns: [
31                { name: "id" },
32                { name: "metadata", nullable: true },
33                { name: "metadata_author", nullable: true },
34                { name: "metadata_note", nullable: true },
35            ],
36        },
37    ]);
38}
39
40/// Round-trip both `Some` and `None`.
41#[driver_test(scenario(crate::scenarios::document_optional_metadata))]
42pub async fn option_embed_crud(test: &mut Test) -> Result<()> {
43    let mut db = setup(test).await;
44
45    toasty::create!(Document {
46        id: "with",
47        metadata: Some(Metadata {
48            author: "alice".to_string(),
49            note: "hi".to_string(),
50        }),
51    })
52    .exec(&mut db)
53    .await?;
54
55    toasty::create!(Document {
56        id: "without",
57        metadata: None,
58    })
59    .exec(&mut db)
60    .await?;
61
62    let with = Document::get_by_id(&mut db, "with").await?;
63    assert_struct!(with.metadata, Some(_ {
64        author: "alice",
65        note: "hi",
66    }));
67
68    let without = Document::get_by_id(&mut db, "without").await?;
69    assert_none!(without.metadata);
70
71    Ok(())
72}
73
74/// `.is_none()` / `.is_some()` filter on the presence column.
75#[driver_test(requires(scan), scenario(crate::scenarios::document_optional_metadata))]
76pub async fn option_embed_filter_presence(test: &mut Test) -> Result<()> {
77    let mut db = setup(test).await;
78
79    toasty::create!(Document {
80        id: "with",
81        metadata: Some(Metadata {
82            author: "alice".to_string(),
83            note: "hi".to_string(),
84        }),
85    })
86    .exec(&mut db)
87    .await?;
88    toasty::create!(Document {
89        id: "without",
90        metadata: None,
91    })
92    .exec(&mut db)
93    .await?;
94
95    let present = Document::filter(Document::fields().metadata().is_some())
96        .exec(&mut db)
97        .await?;
98    assert_struct!(present, [_ { id: "with", .. }]);
99
100    let absent = Document::filter(Document::fields().metadata().is_none())
101        .exec(&mut db)
102        .await?;
103    assert_struct!(absent, [_ { id: "without", .. }]);
104
105    Ok(())
106}
107
108/// Whole-value `.eq(Some(..))` matches only the equal `Some` row, never `None`.
109#[driver_test(requires(scan), scenario(crate::scenarios::document_optional_metadata))]
110pub async fn option_embed_filter_eq(test: &mut Test) -> Result<()> {
111    let mut db = setup(test).await;
112
113    toasty::create!(Document {
114        id: "alice",
115        metadata: Some(Metadata {
116            author: "alice".to_string(),
117            note: "hi".to_string(),
118        }),
119    })
120    .exec(&mut db)
121    .await?;
122    toasty::create!(Document {
123        id: "bob",
124        metadata: Some(Metadata {
125            author: "bob".to_string(),
126            note: "yo".to_string(),
127        }),
128    })
129    .exec(&mut db)
130    .await?;
131    toasty::create!(Document {
132        id: "without",
133        metadata: None,
134    })
135    .exec(&mut db)
136    .await?;
137
138    let matches = Document::filter(Document::fields().metadata().eq(Some(Metadata {
139        author: "alice".to_string(),
140        note: "hi".to_string(),
141    })))
142    .exec(&mut db)
143    .await?;
144    assert_struct!(matches, [_ { id: "alice", .. }]);
145
146    Ok(())
147}
148
149/// Updating the whole value in both directions: `Some` → `None` and `None` → `Some`.
150#[driver_test(scenario(crate::scenarios::document_optional_metadata))]
151pub async fn option_embed_update(test: &mut Test) -> Result<()> {
152    let mut db = setup(test).await;
153
154    toasty::create!(Document {
155        id: "a",
156        metadata: Some(Metadata {
157            author: "alice".to_string(),
158            note: "hi".to_string(),
159        }),
160    })
161    .exec(&mut db)
162    .await?;
163    toasty::create!(Document {
164        id: "b",
165        metadata: None,
166    })
167    .exec(&mut db)
168    .await?;
169
170    // Some -> None
171    Document::filter_by_id("a")
172        .update()
173        .metadata(None)
174        .exec(&mut db)
175        .await?;
176    assert_none!(Document::get_by_id(&mut db, "a").await?.metadata);
177
178    // None -> Some
179    Document::filter_by_id("b")
180        .update()
181        .metadata(Some(Metadata {
182            author: "bob".to_string(),
183            note: "yo".to_string(),
184        }))
185        .exec(&mut db)
186        .await?;
187    assert_struct!(Document::get_by_id(&mut db, "b").await?.metadata, Some(_ {
188        author: "bob",
189        note: "yo",
190    }));
191
192    Ok(())
193}
194
195/// Driver-op coverage for create. A `Some` writes `true` to the presence column
196/// plus the leaf values; a `None` writes `NULL` to the presence column *and*
197/// every leaf column. The column set is identical in both cases — `None` is a
198/// concrete all-`NULL` row, not an omitted/absent one.
199#[driver_test(scenario(crate::scenarios::document_optional_metadata))]
200pub async fn option_embed_create_ops(test: &mut Test) -> Result<()> {
201    let mut db = setup(test).await;
202    let presence = column(&db, "documents", "metadata").index;
203    let author = column(&db, "documents", "metadata_author").index;
204    let note = column(&db, "documents", "metadata_note").index;
205    test.log().clear();
206
207    toasty::create!(Document {
208        id: "a",
209        metadata: Some(Metadata {
210            author: "alice".to_string(),
211            note: "hi".to_string(),
212        }),
213    })
214    .exec(&mut db)
215    .await?;
216    let row = pop_insert(test);
217    assert_eq!(row[&presence], Value::from(true));
218    assert_eq!(row[&author], Value::from("alice"));
219    assert_eq!(row[&note], Value::from("hi"));
220
221    toasty::create!(Document {
222        id: "b",
223        metadata: None,
224    })
225    .exec(&mut db)
226    .await?;
227    let row = pop_insert(test);
228    assert_eq!(row[&presence], Value::Null);
229    assert_eq!(row[&author], Value::Null);
230    assert_eq!(row[&note], Value::Null);
231
232    Ok(())
233}
234
235/// Driver-op coverage for filters. `.is_none()` / `.is_some()` emit a predicate
236/// on the single presence column (`IS NULL` / `NOT (.. IS NULL)`), never a
237/// check distributed across the leaf columns.
238#[driver_test(requires(scan), scenario(crate::scenarios::document_optional_metadata))]
239pub async fn option_embed_filter_ops(test: &mut Test) -> Result<()> {
240    let mut db = setup(test).await;
241    let presence = column(&db, "documents", "metadata").index;
242    test.log().clear();
243
244    // `.is_none()` → `metadata IS NULL`.
245    let _ = Document::filter(Document::fields().metadata().is_none())
246        .exec(&mut db)
247        .await?;
248    assert_struct!(pop_filter(test), Expr::IsNull({
249        expr.as_expr_column_unwrap().column: == presence,
250    }));
251
252    // `.is_some()` → `NOT (metadata IS NULL)`.
253    let _ = Document::filter(Document::fields().metadata().is_some())
254        .exec(&mut db)
255        .await?;
256    let Expr::Not(not) = pop_filter(test) else {
257        panic!("expected NOT");
258    };
259    assert_struct!(*not.expr, Expr::IsNull({
260        expr.as_expr_column_unwrap().column: == presence,
261    }));
262
263    Ok(())
264}
265
266/// Driver-op coverage for updates. Setting `Some` assigns `true` + the leaf
267/// values; setting `None` assigns `NULL` to the presence column and every leaf.
268#[driver_test(scenario(crate::scenarios::document_optional_metadata))]
269pub async fn option_embed_update_ops(test: &mut Test) -> Result<()> {
270    let mut db = setup(test).await;
271    let presence = column(&db, "documents", "metadata").index;
272    let author = column(&db, "documents", "metadata_author").index;
273    let note = column(&db, "documents", "metadata_note").index;
274
275    toasty::create!(Document {
276        id: "a",
277        metadata: None,
278    })
279    .exec(&mut db)
280    .await?;
281
282    // None -> Some.
283    test.log().clear();
284    Document::filter_by_id("a")
285        .update()
286        .metadata(Some(Metadata {
287            author: "alice".to_string(),
288            note: "hi".to_string(),
289        }))
290        .exec(&mut db)
291        .await?;
292    let set = pop_update(test);
293    assert_eq!(set[&presence], Value::from(true));
294    assert_eq!(set[&author], Value::from("alice"));
295    assert_eq!(set[&note], Value::from("hi"));
296
297    // Some -> None.
298    test.log().clear();
299    Document::filter_by_id("a")
300        .update()
301        .metadata(None)
302        .exec(&mut db)
303        .await?;
304    let set = pop_update(test);
305    assert_eq!(set[&presence], Value::Null);
306    assert_eq!(set[&author], Value::Null);
307    assert_eq!(set[&note], Value::Null);
308
309    Ok(())
310}
311
312// ---- Single-field newtype embed: `Option<Code>` over `struct Code(String)`.
313//
314// Regression coverage: a dedicated `bool` presence column would be named after
315// the field (`code`), colliding with the newtype's flattened leaf (also `code`)
316// and breaking `CREATE TABLE`. The nullable head instead reuses the single leaf
317// column, so the embed maps to exactly one nullable column, like
318// `Option<scalar>`. Every test here also implicitly exercises that `CREATE
319// TABLE` succeeds — `setup` would fail on the duplicate column otherwise.
320
321/// The DB schema: exactly one nullable column named after the field, with no
322/// separate presence column.
323#[driver_test(scenario(crate::scenarios::document_optional_code))]
324pub async fn option_newtype_db_schema(test: &mut Test) {
325    let db = setup(test).await;
326    let schema = db.schema();
327
328    assert_struct!(schema.db.tables, [
329        {
330            name: =~ r"documents$",
331            columns: [
332                { name: "id" },
333                { name: "code", nullable: true },
334            ],
335        },
336    ]);
337}
338
339/// Round-trip both `Some` and `None`.
340#[driver_test(scenario(crate::scenarios::document_optional_code))]
341pub async fn option_newtype_crud(test: &mut Test) -> Result<()> {
342    let mut db = setup(test).await;
343
344    toasty::create!(Document {
345        id: "with",
346        code: Some(Code("abc".to_string())),
347    })
348    .exec(&mut db)
349    .await?;
350    toasty::create!(Document {
351        id: "without",
352        code: None,
353    })
354    .exec(&mut db)
355    .await?;
356
357    let with = Document::get_by_id(&mut db, "with").await?;
358    assert_struct!(with.code, Some(== Code("abc".to_string())));
359
360    let without = Document::get_by_id(&mut db, "without").await?;
361    assert_none!(without.code);
362
363    Ok(())
364}
365
366/// `.is_none()` / `.is_some()` filter on the reused head (the single leaf)
367/// column.
368#[driver_test(requires(scan), scenario(crate::scenarios::document_optional_code))]
369pub async fn option_newtype_filter_presence(test: &mut Test) -> Result<()> {
370    let mut db = setup(test).await;
371
372    toasty::create!(Document {
373        id: "with",
374        code: Some(Code("abc".to_string())),
375    })
376    .exec(&mut db)
377    .await?;
378    toasty::create!(Document {
379        id: "without",
380        code: None,
381    })
382    .exec(&mut db)
383    .await?;
384
385    let present = Document::filter(Document::fields().code().is_some())
386        .exec(&mut db)
387        .await?;
388    assert_struct!(present, [_ { id: "with", .. }]);
389
390    let absent = Document::filter(Document::fields().code().is_none())
391        .exec(&mut db)
392        .await?;
393    assert_struct!(absent, [_ { id: "without", .. }]);
394
395    Ok(())
396}
397
398/// Driver-op coverage. `Some` writes the inner value to the single `code`
399/// column; `None` writes `NULL` to it. There is no separate presence column.
400#[driver_test(scenario(crate::scenarios::document_optional_code))]
401pub async fn option_newtype_create_ops(test: &mut Test) -> Result<()> {
402    let mut db = setup(test).await;
403    let code = column(&db, "documents", "code").index;
404    test.log().clear();
405
406    toasty::create!(Document {
407        id: "a",
408        code: Some(Code("xyz".to_string())),
409    })
410    .exec(&mut db)
411    .await?;
412    let row = pop_insert(test);
413    assert_eq!(row[&code], Value::from("xyz"));
414
415    toasty::create!(Document {
416        id: "b",
417        code: None,
418    })
419    .exec(&mut db)
420    .await?;
421    let row = pop_insert(test);
422    assert_eq!(row[&code], Value::Null);
423
424    Ok(())
425}
426
427/// Driver-op coverage for filters: `.is_none()` emits `code IS NULL` on the
428/// single reused column.
429#[driver_test(requires(scan), scenario(crate::scenarios::document_optional_code))]
430pub async fn option_newtype_filter_ops(test: &mut Test) -> Result<()> {
431    let mut db = setup(test).await;
432    let code = column(&db, "documents", "code").index;
433    test.log().clear();
434
435    let _ = Document::filter(Document::fields().code().is_none())
436        .exec(&mut db)
437        .await?;
438    assert_struct!(pop_filter(test), Expr::IsNull({
439        expr.as_expr_column_unwrap().column: == code,
440    }));
441
442    Ok(())
443}
444
445/// Updating the whole value in both directions: `Some` → `None` and
446/// `None` → `Some`.
447#[driver_test(scenario(crate::scenarios::document_optional_code))]
448pub async fn option_newtype_update(test: &mut Test) -> Result<()> {
449    let mut db = setup(test).await;
450
451    toasty::create!(Document {
452        id: "a",
453        code: Some(Code("abc".to_string())),
454    })
455    .exec(&mut db)
456    .await?;
457
458    // Some -> None
459    Document::filter_by_id("a")
460        .update()
461        .code(None)
462        .exec(&mut db)
463        .await?;
464    assert_none!(Document::get_by_id(&mut db, "a").await?.code);
465
466    // None -> Some
467    Document::filter_by_id("a")
468        .update()
469        .code(Some(Code("xyz".to_string())))
470        .exec(&mut db)
471        .await?;
472    assert_struct!(
473        Document::get_by_id(&mut db, "a").await?.code,
474        Some(== Code("xyz".to_string()))
475    );
476
477    Ok(())
478}
479
480/// Whole-value `.eq(Some(..))` on the reused head column matches only the equal
481/// `Some` row, never `None`.
482#[driver_test(requires(scan), scenario(crate::scenarios::document_optional_code))]
483pub async fn option_newtype_filter_eq(test: &mut Test) -> Result<()> {
484    let mut db = setup(test).await;
485
486    toasty::create!(Document {
487        id: "alice",
488        code: Some(Code("abc".to_string())),
489    })
490    .exec(&mut db)
491    .await?;
492    toasty::create!(Document {
493        id: "bob",
494        code: Some(Code("xyz".to_string())),
495    })
496    .exec(&mut db)
497    .await?;
498    toasty::create!(Document {
499        id: "without",
500        code: None,
501    })
502    .exec(&mut db)
503    .await?;
504
505    let matches = Document::filter(Document::fields().code().eq(Some(Code("abc".to_string()))))
506        .exec(&mut db)
507        .await?;
508    assert_struct!(matches, [_ { id: "alice", .. }]);
509
510    Ok(())
511}
512
513/// `Option<Newtype>` where the newtype wraps an `Option`
514/// (`MaybeBody(Option<String>)`) has no unambiguous single-column mapping — the
515/// one column can't tell `None` from `Some(MaybeBody(None))`, and a dedicated
516/// presence column would collide with the newtype leaf. It is rejected at
517/// schema build rather than silently collapsing `Some(None)` to `None`.
518#[driver_test]
519pub async fn option_newtype_wrapping_option_rejected(test: &mut Test) {
520    #[derive(Debug, toasty::Model)]
521    #[allow(dead_code)]
522    struct Document {
523        #[key]
524        id: String,
525
526        body: Option<MaybeBody>,
527    }
528
529    #[derive(Debug, toasty::Embed)]
530    struct MaybeBody(Option<String>);
531
532    let err = assert_err!(test.try_setup_db(models!(Document)).await);
533    let msg = err.to_string();
534    assert!(
535        msg.contains("`Document::body`") && msg.contains("newtype wrapping an optional value"),
536        "unexpected error: {msg}"
537    );
538}
539
540/// Contrast with the rejected newtype above: a *named* single field whose inner
541/// is itself nullable (`Wrapper { value: Option<String> }`) keeps the dedicated
542/// presence column, so it round-trips `Some(value: None)` distinctly from
543/// `None` — the disambiguation a single reused column can't provide.
544#[driver_test]
545pub async fn option_struct_nullable_inner_disambiguates(test: &mut Test) -> Result<()> {
546    #[derive(Debug, toasty::Model)]
547    #[allow(dead_code)]
548    struct Document {
549        #[key]
550        id: String,
551
552        wrapper: Option<Wrapper>,
553    }
554
555    #[derive(Debug, toasty::Embed)]
556    struct Wrapper {
557        value: Option<String>,
558    }
559
560    let mut db = test.setup_db(models!(Document)).await;
561
562    // A `bool` presence column plus the nullable leaf — two columns, so `None`
563    // and `Some(value: None)` are distinguishable.
564    assert_struct!(db.schema().db.tables, [
565        {
566            name: =~ r"documents$",
567            columns: [
568                { name: "id" },
569                { name: "wrapper", nullable: true },
570                { name: "wrapper_value", nullable: true },
571            ],
572        },
573    ]);
574
575    toasty::create!(Document {
576        id: "some_none",
577        wrapper: Some(Wrapper { value: None }),
578    })
579    .exec(&mut db)
580    .await?;
581    toasty::create!(Document {
582        id: "none",
583        wrapper: None,
584    })
585    .exec(&mut db)
586    .await?;
587
588    assert_struct!(
589        Document::get_by_id(&mut db, "some_none").await?.wrapper,
590        Some(_ { value: None })
591    );
592    assert_none!(Document::get_by_id(&mut db, "none").await?.wrapper);
593
594    Ok(())
595}