Skip to main content

toasty_driver_integration_suite/tests/
embed_enum_shared_column.rs

1use crate::prelude::*;
2
3/// A field declared `#[shared(name)]` in two variants coalesces into a single
4/// shared, nullable column rather than producing one column per variant. The
5/// table therefore has exactly one `creature_name` column alongside each
6/// variant's own distinct column.
7#[driver_test(scenario(crate::scenarios::character_creature))]
8pub async fn shared_column_db_schema(t: &mut Test) {
9    let db = setup(t).await;
10    let schema = db.schema();
11
12    assert_struct!(schema.db.tables, [
13        {
14            name: =~ r"characters$",
15            columns: [
16                { name: "id" },
17                { name: "creature", nullable: false },
18                // Shared by both Human and Animal — present exactly once.
19                { name: "creature_name", nullable: true },
20                { name: "creature_profession", nullable: true },
21                { name: "creature_species", nullable: true },
22            ],
23        },
24    ]);
25}
26
27/// The `#[shared(name)]` declaration surfaces in the app schema as the field's
28/// shared identifier; both variants' `name` fields carry the same identifier,
29/// which is what drives the column coalescing.
30#[driver_test(scenario(crate::scenarios::character_creature))]
31pub async fn shared_column_schema_fields(t: &mut Test) {
32    let db = setup(t).await;
33    let schema = db.schema();
34
35    let creature = &schema.app.models[&Creature::id()];
36    assert_struct!(creature, toasty::schema::app::Model::EmbeddedEnum({
37        fields: [
38            { name.app: Some("name"), shared: Some(_ { parts: ["name"] }) },
39            { name.app: Some("profession"), shared: None },
40            { name.app: Some("name"), shared: Some(_ { parts: ["name"] }) },
41            { name.app: Some("species"), shared: None },
42        ],
43    }));
44}
45
46#[driver_test]
47pub async fn raw_shared_identifier_uses_bare_name(t: &mut Test) {
48    #[derive(Debug, toasty::Embed)]
49    enum Value {
50        Text {
51            #[shared(r#type)]
52            kind: String,
53        },
54        Number {
55            #[shared(r#type)]
56            kind: String,
57        },
58    }
59
60    #[derive(Debug, toasty::Model)]
61    struct Record {
62        #[key]
63        id: String,
64        value: Value,
65    }
66
67    let db = t.setup_db(models!(Record)).await;
68    let schema = db.schema();
69
70    assert_struct!(schema.app.models[&Value::id()], toasty::schema::app::Model::EmbeddedEnum({
71        fields: [
72            { shared: Some(_ { parts: ["type"] }), .. },
73            { shared: Some(_ { parts: ["type"] }), .. },
74        ],
75    }));
76    assert_struct!(schema.db.tables, [{
77        columns: [
78            { name: "id" },
79            { name: "value" },
80            { name: "value_type" },
81        ],
82        ..
83    }]);
84}
85
86/// Both variants write and read the shared column, while their variant-specific
87/// columns round-trip independently.
88#[driver_test(scenario(crate::scenarios::character_creature))]
89pub async fn shared_column_roundtrip(t: &mut Test) -> Result<()> {
90    let mut db = setup(t).await;
91
92    let human = Character::create()
93        .creature(Creature::Human {
94            name: "Alice".to_string(),
95            profession: "engineer".to_string(),
96        })
97        .exec(&mut db)
98        .await?;
99
100    let animal = Character::create()
101        .creature(Creature::Animal {
102            name: "Rex".to_string(),
103            species: "dog".to_string(),
104        })
105        .exec(&mut db)
106        .await?;
107
108    assert_eq!(
109        Character::get_by_id(&mut db, &human.id).await?.creature,
110        Creature::Human {
111            name: "Alice".to_string(),
112            profession: "engineer".to_string(),
113        }
114    );
115    assert_eq!(
116        Character::get_by_id(&mut db, &animal.id).await?.creature,
117        Creature::Animal {
118            name: "Rex".to_string(),
119            species: "dog".to_string(),
120        }
121    );
122
123    Ok(())
124}
125
126/// Updating the whole enum field — including switching variants — re-encodes the
127/// shared column correctly. The merged per-variant encode must select the arm
128/// matching the *new* discriminant, so the shared column follows the value into
129/// its new variant while the old variant's column is cleared to NULL.
130#[driver_test(scenario(crate::scenarios::character_creature))]
131pub async fn shared_column_update(t: &mut Test) -> Result<()> {
132    let mut db = setup(t).await;
133
134    let mut character = Character::create()
135        .creature(Creature::Human {
136            name: "Bob".to_string(),
137            profession: "builder".to_string(),
138        })
139        .exec(&mut db)
140        .await?;
141
142    // Update within the same variant: only the shared column and the Human
143    // column change.
144    character
145        .update()
146        .creature(Creature::Human {
147            name: "Bobby".to_string(),
148            profession: "architect".to_string(),
149        })
150        .exec(&mut db)
151        .await?;
152
153    assert_eq!(
154        Character::get_by_id(&mut db, &character.id).await?.creature,
155        Creature::Human {
156            name: "Bobby".to_string(),
157            profession: "architect".to_string(),
158        }
159    );
160
161    // Switch variant: the shared `creature_name` column now holds the Animal's
162    // name, the Human column is cleared, and the Animal column is populated.
163    character
164        .update()
165        .creature(Creature::Animal {
166            name: "Whiskers".to_string(),
167            species: "cat".to_string(),
168        })
169        .exec(&mut db)
170        .await?;
171
172    assert_eq!(
173        Character::get_by_id(&mut db, &character.id).await?.creature,
174        Creature::Animal {
175            name: "Whiskers".to_string(),
176            species: "cat".to_string(),
177        }
178    );
179
180    Ok(())
181}
182
183// Mismatched shared-column types are rejected at compile time by the
184// `SameColumnType` obligation the `Embed` derive emits; see the trybuild case
185// `tests/ui/enum_shared_column_type_mismatch.rs`.
186
187/// Both variants store their `name` in the same physical `creature_name`
188/// column. A variant-rooted filter on that column keeps its implicit variant
189/// gate, so `human().name().eq("Bob")` matches only Human rows even though an
190/// Animal stores the same value in the same column — the discriminant
191/// disambiguates the shared column per variant.
192#[driver_test(requires(scan), scenario(crate::scenarios::character_creature))]
193pub async fn shared_column_variant_gated_filter(t: &mut Test) -> Result<()> {
194    let mut db = setup(t).await;
195
196    for (name, profession) in [("Bob", "builder"), ("Alice", "artist")] {
197        Character::create()
198            .creature(Creature::Human {
199                name: name.to_string(),
200                profession: profession.to_string(),
201            })
202            .exec(&mut db)
203            .await?;
204    }
205
206    for (name, species) in [("Bob", "dog"), ("Rex", "cat")] {
207        Character::create()
208            .creature(Creature::Animal {
209                name: name.to_string(),
210                species: species.to_string(),
211            })
212            .exec(&mut db)
213            .await?;
214    }
215
216    // "Bob" lives in `creature_name` for both a Human and an Animal. The gate on
217    // the Human-rooted filter must read that shared column yet exclude the
218    // Animal that shares its value.
219    let human_bobs = Character::filter(Character::fields().creature().human().name().eq("Bob"))
220        .exec(&mut db)
221        .await?;
222    assert_eq!(human_bobs.len(), 1);
223    assert!(matches!(human_bobs[0].creature, Creature::Human { .. }));
224
225    // The same shared column, gated to the Animal variant, finds the Animal
226    // "Bob" — proving the one column genuinely holds both variants' names.
227    let animal_bobs = Character::filter(Character::fields().creature().animal().name().eq("Bob"))
228        .exec(&mut db)
229        .await?;
230    assert_eq!(animal_bobs.len(), 1);
231    assert!(matches!(animal_bobs[0].creature, Creature::Animal { .. }));
232
233    // A name only one variant uses still resolves correctly through the gate.
234    let humans_named_alice =
235        Character::filter(Character::fields().creature().human().name().eq("Alice"))
236            .exec(&mut db)
237            .await?;
238    assert_eq!(humans_named_alice.len(), 1);
239
240    Ok(())
241}
242
243/// OR-ing the two variant-gated predicates on the shared `creature_name` column
244/// is the natural way to query a single shared column across variants: "any
245/// creature named Bob, regardless of variant". This used to panic in the SQL
246/// serializer (issue #1061) because factoring lifted the shared predicate out
247/// from under its variant gates, exposing the decode's unreachable `Error` else
248/// branch.
249#[driver_test(requires(scan), scenario(crate::scenarios::character_creature))]
250pub async fn shared_column_cross_variant_or(t: &mut Test) -> Result<()> {
251    let mut db = setup(t).await;
252
253    Character::create()
254        .creature(Creature::Human {
255            name: "Bob".to_string(),
256            profession: "builder".to_string(),
257        })
258        .exec(&mut db)
259        .await?;
260    Character::create()
261        .creature(Creature::Animal {
262            name: "Bob".to_string(),
263            species: "dog".to_string(),
264        })
265        .exec(&mut db)
266        .await?;
267    Character::create()
268        .creature(Creature::Animal {
269            name: "Rex".to_string(),
270            species: "cat".to_string(),
271        })
272        .exec(&mut db)
273        .await?;
274
275    // Both a Human "Bob" and an Animal "Bob" live in the shared column; the
276    // cross-variant OR finds both while excluding "Rex".
277    let bobs = Character::filter(
278        Character::fields()
279            .creature()
280            .human()
281            .name()
282            .eq("Bob")
283            .or(Character::fields().creature().animal().name().eq("Bob")),
284    )
285    .exec(&mut db)
286    .await?;
287    assert_eq!(bobs.len(), 2);
288
289    Ok(())
290}