Skip to main content

toasty_driver_integration_suite/tests/
embed_enum_unit.rs

1use toasty::schema::{
2    app::FieldTy,
3    mapping::{self, FieldEnum, FieldPrimitive},
4};
5
6use crate::{helpers::column, prelude::*};
7
8use toasty_core::{
9    driver::Operation,
10    stmt::{Assignment, BinaryOp, Expr, ExprSet, Statement},
11};
12
13/// Tests basic CRUD operations with an embedded enum field.
14/// Validates create, read, update (both instance and query-based), and delete.
15/// The enum discriminant is stored as an INTEGER column and reconstructed on load.
16/// On SQL backends, also verifies the driver-level representation: column names and
17/// discriminant values stored as I64 with no record wrapping.
18#[driver_test(id(ID))]
19pub async fn create_and_query_enum(t: &mut Test) -> Result<()> {
20    #[derive(Debug, PartialEq, toasty::Embed)]
21    enum Status {
22        #[column(variant = 1)]
23        Pending,
24        #[column(variant = 2)]
25        Active,
26        #[column(variant = 3)]
27        Done,
28    }
29
30    #[derive(Debug, toasty::Model)]
31    struct User {
32        #[key]
33        #[auto]
34        id: ID,
35        name: String,
36        status: Status,
37    }
38
39    let mut db = t.setup_db(models!(User, Status)).await;
40    let user_table = table_id(&db, "users");
41
42    // Create: enum variant is stored as its discriminant (1 = Pending)
43    t.log().clear();
44
45    let mut user = User::create()
46        .name("Alice")
47        .status(Status::Pending)
48        .exec(&mut db)
49        .await?;
50
51    // Verify column list and that the discriminant is stored as I64, not a string or record.
52    //
53    // Position: id_u64 uses Expr::Default (no param), so status is at
54    // params[1] (name, status). id_uuid adds the uuid at params[0], shifting
55    // status to params[2].
56    let sql = t.capability().sql;
57    let status_pos = if driver_test_cfg!(id_u64) { 1 } else { 2 };
58    let status_pat = if sql {
59        ArgOr::Arg(status_pos)
60    } else {
61        ArgOr::Value(1i64)
62    };
63    let op = t.log().pop_op();
64    assert_struct!(op, Operation::QuerySql({
65        stmt: Statement::Insert({
66            source.body: ExprSet::Values({
67                rows: [=~ (Any, Any, status_pat)],
68            }),
69            target: toasty_core::stmt::InsertTarget::Table({
70                table: == user_table,
71                columns: == columns(&db, "users", &["id", "name", "status"]),
72            }),
73        }),
74    }));
75    if sql {
76        assert_struct!(op, Operation::QuerySql({
77            params[status_pos].value: == 1i64,
78        }));
79    }
80
81    // Read: discriminant is loaded back and converted to the enum variant
82    let found = User::get_by_id(&mut db, &user.id).await?;
83    assert_eq!(found.status, Status::Pending);
84
85    // Update (instance): replace the enum variant
86    t.log().clear();
87    user.update().status(Status::Active).exec(&mut db).await?;
88
89    // Verify the status column receives the new discriminant as I64
90    // Column index 2 is "status"; value I64(2) = Active discriminant
91    if t.capability().sql {
92        assert_struct!(t.log().pop_op(), Operation::QuerySql({
93            stmt: Statement::Update({
94                target: toasty_core::stmt::UpdateTarget::Table(== user_table),
95                assignments: #{ [2]: Assignment::Set(Expr::Arg({ position: 0 }))},
96            }),
97            params: [{ value: == 2i64 }, ..],
98        }));
99    } else {
100        assert_struct!(t.log().pop_op(), Operation::UpdateByKey({
101            table: == user_table,
102            filter: None,
103            keys: _,
104            assignments: #{ [2]: Assignment::Set(== 2i64)},
105            returning: false,
106        }));
107    }
108
109    let found = User::get_by_id(&mut db, &user.id).await?;
110    assert_eq!(found.status, Status::Active);
111
112    // Update (query-based): same replacement via filter builder
113    User::filter_by_id(user.id)
114        .update()
115        .status(Status::Done)
116        .exec(&mut db)
117        .await?;
118
119    let found = User::get_by_id(&mut db, &user.id).await?;
120    assert_eq!(found.status, Status::Done);
121
122    // Delete: cleanup
123    let id = user.id;
124    user.delete().exec(&mut db).await?;
125    assert_err!(User::get_by_id(&mut db, &id).await);
126    Ok(())
127}
128
129/// Tests filtering records by embedded enum variant.
130/// SQL-only: DynamoDB requires a partition key in queries.
131/// Validates that enum fields can be used in WHERE clauses (comparing discriminants),
132/// and verifies the driver-level representation: the WHERE clause compares the status
133/// column to an I64 discriminant, not a string or other type.
134#[driver_test(requires(sql))]
135pub async fn filter_by_enum_variant(t: &mut Test) -> Result<()> {
136    #[derive(Debug, PartialEq, toasty::Embed)]
137    enum Status {
138        #[column(variant = 1)]
139        Pending,
140        #[column(variant = 2)]
141        Active,
142        #[column(variant = 3)]
143        Done,
144    }
145
146    #[derive(Debug, toasty::Model)]
147    #[allow(dead_code)]
148    struct Task {
149        #[key]
150        #[auto]
151        id: uuid::Uuid,
152        name: String,
153        status: Status,
154    }
155
156    let mut db = t.setup_db(models!(Task, Status)).await;
157
158    // Create tasks with different statuses: 1 pending, 2 active, 1 done
159    for (name, status) in [
160        ("Task A", Status::Pending),
161        ("Task B", Status::Active),
162        ("Task C", Status::Active),
163        ("Task D", Status::Done),
164    ] {
165        Task::create()
166            .name(name)
167            .status(status)
168            .exec(&mut db)
169            .await?;
170    }
171
172    let status_col = column(&db, "tasks", "status");
173    t.log().clear();
174
175    // Filter: only Active tasks (discriminant = 2)
176    let active = Task::filter(Task::fields().status().eq(Status::Active))
177        .exec(&mut db)
178        .await?;
179    assert_eq!(active.len(), 2);
180    {
181        let (op, _) = t.log().pop();
182        assert_struct!(op, Operation::QuerySql({
183            stmt: Statement::Query({
184                body: ExprSet::Select({
185                    filter.expr: Some(Expr::BinaryOp({
186                        lhs.as_expr_column_unwrap().column: == status_col.index,
187                        op: BinaryOp::Eq,
188                        *rhs: Expr::Arg({ position: 0 }),
189                    })),
190                }),
191            }),
192            params: [{ value: == 2i64 }],
193        }));
194    }
195
196    // Filter: only Pending tasks (discriminant = 1)
197    let pending = Task::filter(Task::fields().status().eq(Status::Pending))
198        .exec(&mut db)
199        .await?;
200    assert_eq!(pending.len(), 1);
201    assert_eq!(pending[0].name, "Task A");
202    {
203        let (op, _) = t.log().pop();
204        assert_struct!(op, Operation::QuerySql({
205            stmt: Statement::Query({
206                body: ExprSet::Select({
207                    filter.expr: Some(Expr::BinaryOp({
208                        lhs.as_expr_column_unwrap().column: == status_col.index,
209                        op: BinaryOp::Eq,
210                        *rhs: Expr::Arg({ position: 0 }),
211                    })),
212                }),
213            }),
214            params: [{ value: == 1i64 }],
215        }));
216    }
217
218    // Filter: only Done tasks (discriminant = 3)
219    let done = Task::filter(Task::fields().status().eq(Status::Done))
220        .exec(&mut db)
221        .await?;
222    assert_eq!(done.len(), 1);
223    assert_eq!(done[0].name, "Task D");
224    {
225        let (op, _) = t.log().pop();
226        assert_struct!(op, Operation::QuerySql({
227            stmt: Statement::Query({
228                body: ExprSet::Select({
229                    filter.expr: Some(Expr::BinaryOp({
230                        lhs.as_expr_column_unwrap().column: == status_col.index,
231                        op: BinaryOp::Eq,
232                        *rhs: Expr::Arg({ position: 0 }),
233                    })),
234                }),
235            }),
236            params: [{ value: == 3i64 }],
237        }));
238    }
239
240    Ok(())
241}
242
243/// Tests that embedded enums are registered in the app schema but don't create
244/// their own database tables (they're inlined into parent models as a single column).
245#[driver_test]
246pub async fn basic_embedded_enum(test: &mut Test) {
247    #[derive(toasty::Embed)]
248    enum Status {
249        #[column(variant = 1)]
250        Pending,
251        #[column(variant = 2)]
252        Active,
253        #[column(variant = 3)]
254        Done,
255    }
256
257    let db = test.setup_db(models!(Status)).await;
258    let schema = db.schema();
259
260    // Embedded enums exist in app schema as Model::EmbeddedEnum
261    assert_struct!(schema.app.models, #{
262        Status::id(): toasty::schema::app::Model::EmbeddedEnum({
263            name.upper_camel_case(): "Status",
264            variants: [
265                _ { name.upper_camel_case(): "Pending", discriminant: toasty_core::stmt::Value::I64(1), .. },
266                _ { name.upper_camel_case(): "Active", discriminant: toasty_core::stmt::Value::I64(2), .. },
267                _ { name.upper_camel_case(): "Done", discriminant: toasty_core::stmt::Value::I64(3), .. },
268            ],
269        }),
270    });
271
272    // Embedded enums don't create database tables (stored as a column in parent)
273    assert!(schema.db.tables.is_empty());
274}
275
276/// Tests the complete schema generation and mapping for an embedded enum field:
277/// - App schema: enum field with correct type reference
278/// - DB schema: enum field stored as a single INTEGER column
279/// - Mapping: enum field maps directly to a primitive column (discriminant IS the value)
280#[driver_test]
281pub async fn root_model_with_embedded_enum_field(test: &mut Test) {
282    #[derive(toasty::Embed)]
283    enum Status {
284        #[column(variant = 1)]
285        Pending,
286        #[column(variant = 2)]
287        Active,
288        #[column(variant = 3)]
289        Done,
290    }
291
292    #[derive(toasty::Model)]
293    struct User {
294        #[key]
295        id: String,
296        #[allow(dead_code)]
297        status: Status,
298    }
299
300    let db = test.setup_db(models!(User, Status)).await;
301    let schema = db.schema();
302
303    // Both embedded enum and root model exist in app schema
304    assert_struct!(schema.app.models, #{
305        Status::id(): toasty::schema::app::Model::EmbeddedEnum({
306            name.upper_camel_case(): "Status",
307            variants.len(): 3,
308        }),
309        User::id(): toasty::schema::app::Model::Root({
310            name.upper_camel_case(): "User",
311            fields: [
312                { name.app: Some("id") },
313                {
314                    name.app: Some("status"),
315                    ty: FieldTy::Embedded({
316                        target: == Status::id(),
317                    }),
318                },
319            ],
320        }),
321    });
322
323    // Database table has a single INTEGER column for the enum discriminant
324    assert_struct!(schema.db.tables, [
325        {
326            name: =~ r"users$",
327            columns: [
328                { name: "id" },
329                { name: "status" },
330            ],
331        },
332    ]);
333
334    let user = &schema.app.models[&User::id()];
335    let user_table = schema.table_for(user);
336    let user_mapping = &schema.mapping.models[&User::id()];
337
338    assert_struct!(user_mapping, {
339        columns.len(): 2,
340        fields: [
341            mapping::Field::Primitive(FieldPrimitive {
342                column: == user_table.columns[0].id,
343                lowering: 0,
344                ..
345            }),
346            mapping::Field::Enum(FieldEnum {
347                discriminant: FieldPrimitive {
348                    column: == user_table.columns[1].id,
349                    lowering: 1,
350                    ..
351                },
352                variants.len(): 3,
353                ..
354            }),
355        ],
356    });
357}