Skip to main content

toasty_driver_integration_suite/tests/
embed_enum_shared_index.rs

1use crate::prelude::*;
2
3/// An enum-level `#[unique(name)]` referencing a shared logical field produces
4/// one unique DB index on the single shared column.
5#[driver_test]
6pub async fn shared_field_unique_index_schema(test: &mut Test) {
7    #[derive(Debug, toasty::Embed)]
8    #[unique(name)]
9    enum Creature {
10        #[column(variant = 1)]
11        Human {
12            #[shared(name)]
13            name: String,
14            #[allow(dead_code)]
15            profession: String,
16        },
17        #[column(variant = 2)]
18        Animal {
19            #[shared(name)]
20            name: String,
21            #[allow(dead_code)]
22            species: String,
23        },
24    }
25
26    #[derive(Debug, toasty::Model)]
27    struct Character {
28        #[key]
29        id: String,
30        #[allow(dead_code)]
31        creature: Creature,
32    }
33
34    let db = test.setup_db(models!(Character)).await;
35    let schema = db.schema();
36
37    let table = &schema.db.tables[0];
38    let name_col = columns(&db, "characters", &["creature_name"])[0];
39
40    assert_struct!(table.indices, [
41        { primary_key: true },
42        { unique: true, primary_key: false, columns: [{ column: == name_col }] },
43    ]);
44}
45
46/// Uniqueness on a shared column is cross-variant: a `Human` and an `Animal`
47/// with the same name conflict, matching the shared column's un-gated query
48/// semantics. Distinct names on either variant are accepted.
49#[driver_test]
50pub async fn shared_field_unique_enforced_cross_variant(test: &mut Test) -> Result<()> {
51    #[derive(Debug, PartialEq, toasty::Embed)]
52    #[unique(name)]
53    enum Creature {
54        #[column(variant = 1)]
55        Human {
56            #[shared(name)]
57            name: String,
58            profession: String,
59        },
60        #[column(variant = 2)]
61        Animal {
62            #[shared(name)]
63            name: String,
64            species: String,
65        },
66    }
67
68    #[derive(Debug, toasty::Model)]
69    struct Character {
70        #[key]
71        id: String,
72        creature: Creature,
73    }
74
75    let mut db = test.setup_db(models!(Character)).await;
76
77    toasty::create!(Character {
78        id: "1",
79        creature: Creature::Human {
80            name: "Bob".to_string(),
81            profession: "builder".to_string(),
82        }
83    })
84    .exec(&mut db)
85    .await?;
86
87    // An Animal named "Bob" hits the same shared column value — rejected.
88    assert_err!(
89        toasty::create!(Character {
90            id: "2",
91            creature: Creature::Animal {
92                name: "Bob".to_string(),
93                species: "dog".to_string(),
94            }
95        })
96        .exec(&mut db)
97        .await
98    );
99
100    // A different name on either variant is fine.
101    toasty::create!(Character {
102        id: "3",
103        creature: Creature::Animal {
104            name: "Rex".to_string(),
105            species: "dog".to_string(),
106        }
107    })
108    .exec(&mut db)
109    .await?;
110
111    toasty::create!(Character {
112        id: "4",
113        creature: Creature::Human {
114            name: "Alice".to_string(),
115            profession: "artist".to_string(),
116        }
117    })
118    .exec(&mut db)
119    .await?;
120
121    Ok(())
122}
123
124/// A non-unique enum-level `#[index(name)]` on a shared field produces a
125/// non-unique index and permits duplicate values across variants.
126#[driver_test]
127pub async fn shared_field_non_unique_index(test: &mut Test) -> Result<()> {
128    #[derive(Debug, PartialEq, toasty::Embed)]
129    #[index(name)]
130    enum Creature {
131        #[column(variant = 1)]
132        Human {
133            #[shared(name)]
134            name: String,
135        },
136        #[column(variant = 2)]
137        Animal {
138            #[shared(name)]
139            name: String,
140        },
141    }
142
143    #[derive(Debug, toasty::Model)]
144    struct Character {
145        #[key]
146        id: String,
147        creature: Creature,
148    }
149
150    let mut db = test.setup_db(models!(Character)).await;
151
152    let name_col = columns(&db, "characters", &["creature_name"])[0];
153    assert_struct!(db.schema().db.tables[0].indices, [
154        { primary_key: true },
155        { unique: false, primary_key: false, columns: [{ column: == name_col }] },
156    ]);
157
158    // Duplicates across variants are allowed on a non-unique index.
159    toasty::create!(Character {
160        id: "1",
161        creature: Creature::Human {
162            name: "Bob".to_string()
163        }
164    })
165    .exec(&mut db)
166    .await?;
167    toasty::create!(Character {
168        id: "2",
169        creature: Creature::Animal {
170            name: "Bob".to_string()
171        }
172    })
173    .exec(&mut db)
174    .await?;
175
176    Ok(())
177}
178
179/// An enum-level attribute may reference a variant field that owns its column
180/// via a `variant::field` path; combined with a shared field it produces a
181/// composite index. Composite unique indices are SQL-only (DynamoDB does not
182/// support them; see `index_composite`).
183#[driver_test(requires(sql))]
184pub async fn composite_index_shared_and_variant_field(test: &mut Test) -> Result<()> {
185    #[derive(Debug, PartialEq, toasty::Embed)]
186    #[unique(name, human::profession)]
187    enum Creature {
188        #[column(variant = 1)]
189        Human {
190            #[shared(name)]
191            name: String,
192            profession: String,
193        },
194        #[column(variant = 2)]
195        Animal {
196            #[shared(name)]
197            name: String,
198            species: String,
199        },
200    }
201
202    #[derive(Debug, toasty::Model)]
203    struct Character {
204        #[key]
205        id: String,
206        creature: Creature,
207    }
208
209    let mut db = test.setup_db(models!(Character)).await;
210
211    let cols = columns(&db, "characters", &["creature_name", "creature_profession"]);
212    assert_struct!(db.schema().db.tables[0].indices, [
213        { primary_key: true },
214        {
215            unique: true,
216            primary_key: false,
217            columns: [{ column: == cols[0] }, { column: == cols[1] }],
218        },
219    ]);
220
221    toasty::create!(Character {
222        id: "1",
223        creature: Creature::Human {
224            name: "Bob".to_string(),
225            profession: "builder".to_string(),
226        }
227    })
228    .exec(&mut db)
229    .await?;
230
231    // Same (name, profession) combination is rejected.
232    assert_err!(
233        toasty::create!(Character {
234            id: "2",
235            creature: Creature::Human {
236                name: "Bob".to_string(),
237                profession: "builder".to_string(),
238            }
239        })
240        .exec(&mut db)
241        .await
242    );
243
244    // Same name, different profession — allowed; uniqueness is on the pair.
245    toasty::create!(Character {
246        id: "3",
247        creature: Creature::Human {
248            name: "Bob".to_string(),
249            profession: "architect".to_string(),
250        }
251    })
252    .exec(&mut db)
253    .await?;
254
255    Ok(())
256}
257
258/// Two variant fields resolving to the same column name without a common
259/// `#[shared]` identifier are a duplicate column, not an implicit merge. The
260/// macro cannot catch this case (for embedded fields `#[column]` is a
261/// flattening prefix, not a single column), so the schema builder rejects it.
262#[driver_test]
263pub async fn duplicate_column_without_shared_rejected(test: &mut Test) -> Result<()> {
264    #[derive(Debug, toasty::Embed)]
265    enum Value {
266        #[column(variant = 1)]
267        Text {
268            #[allow(dead_code)]
269            #[column("value")]
270            text: String,
271        },
272        #[column(variant = 2)]
273        Number {
274            #[allow(dead_code)]
275            #[column("value")]
276            number: String,
277        },
278    }
279
280    #[derive(Debug, toasty::Model)]
281    struct Holder {
282        #[key]
283        id: String,
284        #[allow(dead_code)]
285        value: Value,
286    }
287
288    let err = assert_err!(test.try_setup_db(models!(Holder)).await);
289    assert!(
290        err.to_string().contains("without declaring a shared field"),
291        "unexpected error: {err}"
292    );
293
294    Ok(())
295}
296
297/// Columns created while mapping a nested enum stay in the outer enum's
298/// namespace: a later variant field of the outer enum whose flattened name
299/// collides with one of the nested enum's columns is a duplicate, not a
300/// silent second column.
301#[driver_test]
302pub async fn nested_enum_column_collision_rejected(test: &mut Test) -> Result<()> {
303    #[derive(Debug, PartialEq, toasty::Embed)]
304    enum Inner {
305        #[column(variant = 1)]
306        A {
307            #[allow(dead_code)]
308            x: String,
309        },
310    }
311
312    #[derive(Debug, PartialEq, toasty::Embed)]
313    enum Outer {
314        #[column(variant = 1)]
315        V1 {
316            #[allow(dead_code)]
317            inner: Inner,
318        },
319        // Flattens to the same column as `Inner::A::x` above
320        // (`{field}_inner_x`).
321        #[column(variant = 2)]
322        V2 {
323            #[allow(dead_code)]
324            inner_x: String,
325        },
326    }
327
328    #[derive(Debug, toasty::Model)]
329    struct Holder {
330        #[key]
331        id: String,
332        #[allow(dead_code)]
333        value: Outer,
334    }
335
336    let err = assert_err!(test.try_setup_db(models!(Holder)).await);
337    assert!(
338        err.to_string().contains("without declaring a shared field"),
339        "unexpected error: {err}"
340    );
341
342    Ok(())
343}
344
345/// Same collision as above, opposite order: the plain variant field comes
346/// first and the nested enum second, so the nested enum's variant fields must
347/// be checked against column names the outer enum already created.
348#[driver_test]
349pub async fn nested_enum_column_collision_rejected_reverse_order(test: &mut Test) -> Result<()> {
350    #[derive(Debug, PartialEq, toasty::Embed)]
351    enum Inner {
352        #[column(variant = 1)]
353        A {
354            #[allow(dead_code)]
355            x: String,
356        },
357    }
358
359    #[derive(Debug, PartialEq, toasty::Embed)]
360    enum Outer {
361        // Flattens to the same column as `Inner::A::x` below
362        // (`{field}_inner_x`).
363        #[column(variant = 1)]
364        V1 {
365            #[allow(dead_code)]
366            inner_x: String,
367        },
368        #[column(variant = 2)]
369        V2 {
370            #[allow(dead_code)]
371            inner: Inner,
372        },
373    }
374
375    #[derive(Debug, toasty::Model)]
376    struct Holder {
377        #[key]
378        id: String,
379        #[allow(dead_code)]
380        value: Outer,
381    }
382
383    let err = assert_err!(test.try_setup_db(models!(Holder)).await);
384    assert!(
385        err.to_string().contains("without declaring a shared field"),
386        "unexpected error: {err}"
387    );
388
389    Ok(())
390}
391
392/// Embedded-struct variant fields sharing a `#[column("...")]` *prefix* are
393/// not a collision: the string prefixes each flattened leaf, so differently
394/// shaped embeds produce disjoint columns.
395#[driver_test]
396pub async fn embedded_prefix_collision_is_not_duplicate(test: &mut Test) -> Result<()> {
397    #[derive(Debug, PartialEq, toasty::Embed)]
398    struct Geo {
399        lat: String,
400    }
401
402    #[derive(Debug, PartialEq, toasty::Embed)]
403    struct Postal {
404        zip: String,
405    }
406
407    #[derive(Debug, PartialEq, toasty::Embed)]
408    enum Location {
409        #[column(variant = 1)]
410        Coords {
411            #[column("common")]
412            geo: Geo,
413        },
414        #[column(variant = 2)]
415        Mail {
416            #[column("common")]
417            postal: Postal,
418        },
419    }
420
421    #[derive(Debug, toasty::Model)]
422    struct Place {
423        #[key]
424        id: String,
425        location: Location,
426    }
427
428    let mut db = test.setup_db(models!(Place)).await;
429
430    // Disjoint flattened columns, no duplicate-column error.
431    let table = &db.schema().db.tables[0];
432    assert!(
433        table
434            .columns
435            .iter()
436            .any(|c| c.name == "location_common_lat")
437    );
438    assert!(
439        table
440            .columns
441            .iter()
442            .any(|c| c.name == "location_common_zip")
443    );
444
445    toasty::create!(Place {
446        id: "1",
447        location: Location::Coords {
448            geo: Geo {
449                lat: "47.6".to_string()
450            }
451        }
452    })
453    .exec(&mut db)
454    .await?;
455
456    let place = Place::get_by_id(&mut db, &"1".to_string()).await?;
457    assert!(matches!(place.location, Location::Coords { .. }));
458
459    Ok(())
460}
461
462/// A `#[column("...")]` override on a shared group renames the shared column;
463/// the shared identifier keeps naming the field in `#[unique(...)]`, and the
464/// index lands on the renamed column. The override needs declaring on only one
465/// member of the group.
466#[driver_test]
467pub async fn shared_field_column_override(test: &mut Test) -> Result<()> {
468    #[derive(Debug, PartialEq, toasty::Embed)]
469    #[unique(name)]
470    enum Creature {
471        #[column(variant = 1)]
472        Human {
473            #[shared(name)]
474            #[column("legacy_name")]
475            name: String,
476        },
477        #[column(variant = 2)]
478        Animal {
479            #[shared(name)]
480            name: String,
481        },
482    }
483
484    #[derive(Debug, toasty::Model)]
485    struct Character {
486        #[key]
487        id: String,
488        creature: Creature,
489    }
490
491    let mut db = test.setup_db(models!(Character)).await;
492
493    // One shared column under the overridden name; no `creature_name` column.
494    let table = &db.schema().db.tables[0];
495    assert!(
496        table
497            .columns
498            .iter()
499            .any(|c| c.name == "creature_legacy_name")
500    );
501    assert!(!table.columns.iter().any(|c| c.name == "creature_name"));
502
503    let legacy_col = columns(&db, "characters", &["creature_legacy_name"])[0];
504    assert_struct!(table.indices, [
505        { primary_key: true },
506        { unique: true, primary_key: false, columns: [{ column: == legacy_col }] },
507    ]);
508
509    // Both variants round-trip through the renamed shared column.
510    toasty::create!(Character {
511        id: "1",
512        creature: Creature::Human {
513            name: "Bob".to_string()
514        }
515    })
516    .exec(&mut db)
517    .await?;
518
519    assert_struct!(
520        Character::get_by_id(&mut db, &"1".to_string()).await?,
521        _ { creature: Creature::Human { name: "Bob", .. }, .. }
522    );
523
524    Ok(())
525}