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/// Both variants write and read the shared column, while their variant-specific
86/// columns round-trip independently.
87#[driver_test(scenario(crate::scenarios::character_creature))]
88pub async fn shared_column_roundtrip(t: &mut Test) -> Result<()> {
89    let mut db = setup(t).await;
90
91    let human = Character::create()
92        .creature(Creature::Human {
93            name: "Alice".to_string(),
94            profession: "engineer".to_string(),
95        })
96        .exec(&mut db)
97        .await?;
98
99    let animal = Character::create()
100        .creature(Creature::Animal {
101            name: "Rex".to_string(),
102            species: "dog".to_string(),
103        })
104        .exec(&mut db)
105        .await?;
106
107    assert_eq!(
108        Character::get_by_id(&mut db, &human.id).await?.creature,
109        Creature::Human {
110            name: "Alice".to_string(),
111            profession: "engineer".to_string(),
112        }
113    );
114    assert_eq!(
115        Character::get_by_id(&mut db, &animal.id).await?.creature,
116        Creature::Animal {
117            name: "Rex".to_string(),
118            species: "dog".to_string(),
119        }
120    );
121
122    Ok(())
123}
124
125/// Updating the whole enum field — including switching variants — re-encodes the
126/// shared column correctly. The merged per-variant encode must select the arm
127/// matching the *new* discriminant, so the shared column follows the value into
128/// its new variant while the old variant's column is cleared to NULL.
129#[driver_test(scenario(crate::scenarios::character_creature))]
130pub async fn shared_column_update(t: &mut Test) -> Result<()> {
131    let mut db = setup(t).await;
132
133    let mut character = Character::create()
134        .creature(Creature::Human {
135            name: "Bob".to_string(),
136            profession: "builder".to_string(),
137        })
138        .exec(&mut db)
139        .await?;
140
141    // Update within the same variant: only the shared column and the Human
142    // column change.
143    character
144        .update()
145        .creature(Creature::Human {
146            name: "Bobby".to_string(),
147            profession: "architect".to_string(),
148        })
149        .exec(&mut db)
150        .await?;
151
152    assert_eq!(
153        Character::get_by_id(&mut db, &character.id).await?.creature,
154        Creature::Human {
155            name: "Bobby".to_string(),
156            profession: "architect".to_string(),
157        }
158    );
159
160    // Switch variant: the shared `creature_name` column now holds the Animal's
161    // name, the Human column is cleared, and the Animal column is populated.
162    character
163        .update()
164        .creature(Creature::Animal {
165            name: "Whiskers".to_string(),
166            species: "cat".to_string(),
167        })
168        .exec(&mut db)
169        .await?;
170
171    assert_eq!(
172        Character::get_by_id(&mut db, &character.id).await?.creature,
173        Creature::Animal {
174            name: "Whiskers".to_string(),
175            species: "cat".to_string(),
176        }
177    );
178
179    Ok(())
180}
181
182// Mismatched shared-column types are rejected at compile time by the
183// `SameColumnType` obligation the `Embed` derive emits; see the trybuild case
184// `tests/ui/enum_shared_column_type_mismatch.rs`.
185
186/// Both variants store their `name` in the same physical `creature_name`
187/// column. A variant-rooted filter on that column keeps its implicit variant
188/// gate, so `human().name().eq("Bob")` matches only Human rows even though an
189/// Animal stores the same value in the same column — the discriminant
190/// disambiguates the shared column per variant.
191#[driver_test(requires(scan), scenario(crate::scenarios::character_creature))]
192pub async fn shared_column_variant_gated_filter(t: &mut Test) -> Result<()> {
193    let mut db = setup(t).await;
194
195    for (name, profession) in [("Bob", "builder"), ("Alice", "artist")] {
196        Character::create()
197            .creature(Creature::Human {
198                name: name.to_string(),
199                profession: profession.to_string(),
200            })
201            .exec(&mut db)
202            .await?;
203    }
204
205    for (name, species) in [("Bob", "dog"), ("Rex", "cat")] {
206        Character::create()
207            .creature(Creature::Animal {
208                name: name.to_string(),
209                species: species.to_string(),
210            })
211            .exec(&mut db)
212            .await?;
213    }
214
215    // "Bob" lives in `creature_name` for both a Human and an Animal. The gate on
216    // the Human-rooted filter must read that shared column yet exclude the
217    // Animal that shares its value.
218    let human_bobs = Character::filter(Character::fields().creature().human().name().eq("Bob"))
219        .exec(&mut db)
220        .await?;
221    assert_eq!(human_bobs.len(), 1);
222    assert!(matches!(human_bobs[0].creature, Creature::Human { .. }));
223
224    // The same shared column, gated to the Animal variant, finds the Animal
225    // "Bob" — proving the one column genuinely holds both variants' names.
226    let animal_bobs = Character::filter(Character::fields().creature().animal().name().eq("Bob"))
227        .exec(&mut db)
228        .await?;
229    assert_eq!(animal_bobs.len(), 1);
230    assert!(matches!(animal_bobs[0].creature, Creature::Animal { .. }));
231
232    // A name only one variant uses still resolves correctly through the gate.
233    let humans_named_alice =
234        Character::filter(Character::fields().creature().human().name().eq("Alice"))
235            .exec(&mut db)
236            .await?;
237    assert_eq!(humans_named_alice.len(), 1);
238
239    Ok(())
240}
241
242/// OR-ing the two variant-gated predicates on the shared `creature_name` column
243/// is the natural way to query a single shared column across variants: "any
244/// creature named Bob, regardless of variant". This used to panic in the SQL
245/// serializer (issue #1061) because factoring lifted the shared predicate out
246/// from under its variant gates, exposing the decode's unreachable `Error` else
247/// branch.
248#[driver_test(requires(scan), scenario(crate::scenarios::character_creature))]
249pub async fn shared_column_cross_variant_or(t: &mut Test) -> Result<()> {
250    let mut db = setup(t).await;
251
252    Character::create()
253        .creature(Creature::Human {
254            name: "Bob".to_string(),
255            profession: "builder".to_string(),
256        })
257        .exec(&mut db)
258        .await?;
259    Character::create()
260        .creature(Creature::Animal {
261            name: "Bob".to_string(),
262            species: "dog".to_string(),
263        })
264        .exec(&mut db)
265        .await?;
266    Character::create()
267        .creature(Creature::Animal {
268            name: "Rex".to_string(),
269            species: "cat".to_string(),
270        })
271        .exec(&mut db)
272        .await?;
273
274    // Both a Human "Bob" and an Animal "Bob" live in the shared column; the
275    // cross-variant OR finds both while excluding "Rex".
276    let bobs = Character::filter(
277        Character::fields()
278            .creature()
279            .human()
280            .name()
281            .eq("Bob")
282            .or(Character::fields().creature().animal().name().eq("Bob")),
283    )
284    .exec(&mut db)
285    .await?;
286    assert_eq!(bobs.len(), 2);
287
288    Ok(())
289}