Skip to main content

toasty_driver_integration_suite/tests/
embed_enum_index.rs

1use crate::prelude::*;
2
3/// Tests that `#[unique]` and `#[index]` on embedded enum variant fields produce
4/// physical DB indices on the flattened columns.
5#[driver_test]
6pub async fn embedded_enum_index_schema(test: &mut Test) {
7    #[derive(Debug, toasty::Embed)]
8    enum ContactInfo {
9        #[column(variant = 1)]
10        Email {
11            #[unique]
12            address: String,
13        },
14        #[column(variant = 2)]
15        Phone {
16            #[index]
17            number: String,
18        },
19    }
20
21    #[derive(Debug, toasty::Model)]
22    struct User {
23        #[key]
24        id: String,
25        name: String,
26        #[allow(dead_code)]
27        contact: ContactInfo,
28    }
29
30    let db = test.setup_db(models!(User)).await;
31    let schema = db.schema();
32
33    // The embedded enum should carry its indices in the app schema
34    assert_struct!(schema.app.models, #{
35        ContactInfo::id(): toasty::schema::app::Model::EmbeddedEnum({
36            indices.len(): 2,
37        }),
38        ..
39    });
40
41    // The DB table should have indices on the flattened variant field columns.
42    // Index 0: primary key (id)
43    // Index 1: unique on contact_address
44    // Index 2: non-unique on contact_number
45    let table = &schema.db.tables[0];
46    let address_col = columns(&db, "users", &["contact_address"])[0];
47    let number_col = columns(&db, "users", &["contact_number"])[0];
48
49    assert_struct!(table.indices, [
50        // PK
51        { primary_key: true },
52        // Unique index on contact_address
53        { unique: true, primary_key: false, columns: [{ column: == address_col }] },
54        // Non-unique index on contact_number
55        { unique: false, primary_key: false, columns: [{ column: == number_col }] },
56    ]);
57}
58
59/// Tests that unique constraint on embedded enum variant field is enforced at
60/// the database level.
61#[driver_test]
62pub async fn embedded_enum_unique_index_enforced(test: &mut Test) -> Result<()> {
63    #[derive(Debug, toasty::Embed)]
64    enum ContactInfo {
65        #[column(variant = 1)]
66        Email {
67            #[unique]
68            address: String,
69        },
70        #[column(variant = 2)]
71        Phone { number: String },
72    }
73
74    #[derive(Debug, toasty::Model)]
75    struct User {
76        #[key]
77        id: String,
78        name: String,
79        contact: ContactInfo,
80    }
81
82    let mut db = test.setup_db(models!(User)).await;
83
84    // Create a user with an email contact
85    User::create()
86        .id("1")
87        .name("Alice")
88        .contact(ContactInfo::Email {
89            address: "alice@example.com".to_string(),
90        })
91        .exec(&mut db)
92        .await?;
93
94    // Creating another user with the same email address should fail
95    assert_err!(
96        User::create()
97            .id("2")
98            .name("Bob")
99            .contact(ContactInfo::Email {
100                address: "alice@example.com".to_string(),
101            })
102            .exec(&mut db)
103            .await
104    );
105
106    // Creating a user with a different email works
107    User::create()
108        .id("3")
109        .name("Charlie")
110        .contact(ContactInfo::Email {
111            address: "charlie@example.com".to_string(),
112        })
113        .exec(&mut db)
114        .await?;
115
116    // Creating a user with a phone contact works (different variant, no unique on number)
117    User::create()
118        .id("4")
119        .name("Dave")
120        .contact(ContactInfo::Phone {
121            number: "555-1234".to_string(),
122        })
123        .exec(&mut db)
124        .await?;
125
126    // Filter by the indexed variant field
127    let users = User::filter(
128        User::fields()
129            .contact()
130            .email()
131            .matches(|e| e.address().eq("alice@example.com")),
132    )
133    .exec(&mut db)
134    .await?;
135
136    assert_struct!(users, [{ name: "Alice" }]);
137
138    Ok(())
139}
140
141/// Regression test for #973: a unit (data-less) embedded enum used as a model
142/// field can be indexed. The index targets the enum's discriminant column.
143///
144/// Before the fix, building the schema panicked because the index-column
145/// resolver had no case for enum mappings.
146#[driver_test]
147pub async fn unit_enum_field_index_schema(test: &mut Test) {
148    #[derive(Debug, toasty::Embed)]
149    enum EntityType {
150        Fund,
151        Account,
152        Department,
153        Program,
154    }
155
156    #[derive(Debug, toasty::Model)]
157    #[index(entity_type)]
158    struct Registry {
159        #[key]
160        id: String,
161        #[allow(dead_code)]
162        entity_type: EntityType,
163    }
164
165    let db = test.setup_db(models!(Registry)).await;
166    let schema = db.schema();
167
168    // The enum field is stored as a single discriminant column; the index
169    // targets that column.
170    let table = &schema.db.tables[0];
171    let entity_type_col = table
172        .columns
173        .iter()
174        .find(|c| c.name == "entity_type")
175        .expect("entity_type discriminant column should exist");
176
177    // Index 0: primary key (id). Index 1: non-unique on the discriminant column.
178    assert_struct!(table.indices, [
179        { primary_key: true },
180        { unique: false, primary_key: false, columns: [{ column: == entity_type_col.id }] },
181    ]);
182}
183
184/// Regression test for #973: queries filtering on an indexed unit enum field
185/// return the correct rows across all drivers.
186#[driver_test]
187pub async fn unit_enum_field_index_filter(test: &mut Test) -> Result<()> {
188    #[derive(Debug, PartialEq, toasty::Embed)]
189    enum EntityType {
190        Fund,
191        Account,
192        Department,
193    }
194
195    #[derive(Debug, toasty::Model)]
196    #[index(entity_type)]
197    struct Registry {
198        #[key]
199        id: String,
200        entity_type: EntityType,
201    }
202
203    let mut db = test.setup_db(models!(Registry)).await;
204
205    toasty::create!(Registry::[
206        { id: "1", entity_type: EntityType::Fund },
207        { id: "2", entity_type: EntityType::Account },
208        { id: "3", entity_type: EntityType::Fund },
209        { id: "4", entity_type: EntityType::Department },
210    ])
211    .exec(&mut db)
212    .await?;
213
214    let mut funds = Registry::filter(Registry::fields().entity_type().eq(EntityType::Fund))
215        .exec(&mut db)
216        .await?;
217    funds.sort_by(|a, b| a.id.cmp(&b.id));
218
219    assert_struct!(funds, [{ id: "1" }, { id: "3" }]);
220
221    Ok(())
222}
223
224/// Regression test for #973: a unit embedded enum field can participate in a
225/// composite `#[unique(...)]` constraint alongside scalar fields, mirroring the
226/// `#[unique(dimension_type_id, entity_type, entity_id)]` case from the issue.
227///
228/// DynamoDB does not support composite unique indices (see
229/// `composite_unique_index_unsupported_on_dynamodb` in `index_composite`), so
230/// this is SQL-only.
231#[driver_test(requires(sql))]
232pub async fn unit_enum_composite_unique_enforced(test: &mut Test) -> Result<()> {
233    #[derive(Debug, PartialEq, toasty::Embed)]
234    enum EntityType {
235        Fund,
236        Account,
237    }
238
239    #[derive(Debug, toasty::Model)]
240    #[unique(entity_type, entity_id)]
241    struct Registry {
242        #[key]
243        #[auto]
244        id: u64,
245        entity_type: EntityType,
246        entity_id: String,
247    }
248
249    let mut db = test.setup_db(models!(Registry)).await;
250
251    // The non-primary-key index is unique and spans both columns (the enum's
252    // discriminant column plus entity_id).
253    let index = db.schema().db.tables[0]
254        .indices
255        .iter()
256        .find(|i| !i.primary_key)
257        .expect("composite unique index");
258    assert!(index.unique);
259    assert_eq!(index.columns.len(), 2);
260
261    toasty::create!(Registry {
262        entity_type: EntityType::Fund,
263        entity_id: "x"
264    })
265    .exec(&mut db)
266    .await?;
267
268    // The same (entity_type, entity_id) combination is rejected.
269    assert_err!(
270        toasty::create!(Registry {
271            entity_type: EntityType::Fund,
272            entity_id: "x"
273        })
274        .exec(&mut db)
275        .await
276    );
277
278    // Differing in either column is allowed — uniqueness is on the combination.
279    toasty::create!(Registry {
280        entity_type: EntityType::Account,
281        entity_id: "x"
282    })
283    .exec(&mut db)
284    .await?;
285
286    toasty::create!(Registry {
287        entity_type: EntityType::Fund,
288        entity_id: "y"
289    })
290    .exec(&mut db)
291    .await?;
292
293    Ok(())
294}