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, Value},
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/// Validates that enum fields can be used in WHERE clauses (comparing discriminants),
131/// and verifies the driver-level representation: the predicate compares the status
132/// column to an I64 discriminant, not a string or other type. On SQL the predicate
133/// is emitted as `column = $0` with an I64 param; on DynamoDB it lowers to a
134/// `Scan` whose filter inlines the I64 value directly.
135#[driver_test(requires(scan))]
136pub async fn filter_by_enum_variant(t: &mut Test) -> Result<()> {
137    #[derive(Debug, PartialEq, toasty::Embed)]
138    enum Status {
139        #[column(variant = 1)]
140        Pending,
141        #[column(variant = 2)]
142        Active,
143        #[column(variant = 3)]
144        Done,
145    }
146
147    #[derive(Debug, toasty::Model)]
148    #[allow(dead_code)]
149    struct Task {
150        #[key]
151        #[auto]
152        id: uuid::Uuid,
153        name: String,
154        status: Status,
155    }
156
157    let mut db = t.setup_db(models!(Task, Status)).await;
158
159    // Create tasks with different statuses: 1 pending, 2 active, 1 done
160    for (name, status) in [
161        ("Task A", Status::Pending),
162        ("Task B", Status::Active),
163        ("Task C", Status::Active),
164        ("Task D", Status::Done),
165    ] {
166        Task::create()
167            .name(name)
168            .status(status)
169            .exec(&mut db)
170            .await?;
171    }
172
173    let status_col = column(&db, "tasks", "status");
174    t.log().clear();
175
176    // Filter: only Active tasks (discriminant = 2)
177    let active = Task::filter(Task::fields().status().eq(Status::Active))
178        .exec(&mut db)
179        .await?;
180    assert_eq!(active.len(), 2);
181    {
182        let (op, _) = t.log().pop();
183        if t.capability().sql {
184            assert_struct!(op, Operation::QuerySql({
185                stmt: Statement::Query({
186                    body: ExprSet::Select({
187                        filter.expr: Some(Expr::BinaryOp({
188                            lhs.as_expr_column_unwrap().column: == status_col.index,
189                            op: BinaryOp::Eq,
190                            *rhs: Expr::Arg({ position: 0 }),
191                        })),
192                    }),
193                }),
194                params: [{ value: == 2i64 }],
195            }));
196        } else {
197            assert_struct!(op, Operation::Scan({
198                filter: Some(Expr::BinaryOp({
199                    lhs.as_expr_column_unwrap().column: == status_col.index,
200                    op: BinaryOp::Eq,
201                    *rhs: Expr::Value(== Value::I64(2)),
202                })),
203            }));
204        }
205    }
206
207    // Filter: only Pending tasks (discriminant = 1)
208    let pending = Task::filter(Task::fields().status().eq(Status::Pending))
209        .exec(&mut db)
210        .await?;
211    assert_eq!(pending.len(), 1);
212    assert_eq!(pending[0].name, "Task A");
213    {
214        let (op, _) = t.log().pop();
215        if t.capability().sql {
216            assert_struct!(op, Operation::QuerySql({
217                stmt: Statement::Query({
218                    body: ExprSet::Select({
219                        filter.expr: Some(Expr::BinaryOp({
220                            lhs.as_expr_column_unwrap().column: == status_col.index,
221                            op: BinaryOp::Eq,
222                            *rhs: Expr::Arg({ position: 0 }),
223                        })),
224                    }),
225                }),
226                params: [{ value: == 1i64 }],
227            }));
228        } else {
229            assert_struct!(op, Operation::Scan({
230                filter: Some(Expr::BinaryOp({
231                    lhs.as_expr_column_unwrap().column: == status_col.index,
232                    op: BinaryOp::Eq,
233                    *rhs: Expr::Value(== Value::I64(1)),
234                })),
235            }));
236        }
237    }
238
239    // Filter: only Done tasks (discriminant = 3)
240    let done = Task::filter(Task::fields().status().eq(Status::Done))
241        .exec(&mut db)
242        .await?;
243    assert_eq!(done.len(), 1);
244    assert_eq!(done[0].name, "Task D");
245    {
246        let (op, _) = t.log().pop();
247        if t.capability().sql {
248            assert_struct!(op, Operation::QuerySql({
249                stmt: Statement::Query({
250                    body: ExprSet::Select({
251                        filter.expr: Some(Expr::BinaryOp({
252                            lhs.as_expr_column_unwrap().column: == status_col.index,
253                            op: BinaryOp::Eq,
254                            *rhs: Expr::Arg({ position: 0 }),
255                        })),
256                    }),
257                }),
258                params: [{ value: == 3i64 }],
259            }));
260        } else {
261            assert_struct!(op, Operation::Scan({
262                filter: Some(Expr::BinaryOp({
263                    lhs.as_expr_column_unwrap().column: == status_col.index,
264                    op: BinaryOp::Eq,
265                    *rhs: Expr::Value(== Value::I64(3)),
266                })),
267            }));
268        }
269    }
270
271    Ok(())
272}
273
274/// Tests that embedded enums are registered in the app schema but don't create
275/// their own database tables (they're inlined into parent models as a single column).
276#[driver_test]
277pub async fn basic_embedded_enum(test: &mut Test) {
278    #[derive(toasty::Embed)]
279    enum Status {
280        #[column(variant = 1)]
281        Pending,
282        #[column(variant = 2)]
283        Active,
284        #[column(variant = 3)]
285        Done,
286    }
287
288    let db = test.setup_db(models!(Status)).await;
289    let schema = db.schema();
290
291    // Embedded enums exist in app schema as Model::EmbeddedEnum
292    assert_struct!(schema.app.models, #{
293        Status::id(): toasty::schema::app::Model::EmbeddedEnum({
294            name.upper_camel_case(): "Status",
295            variants: [
296                _ { name.upper_camel_case(): "Pending", discriminant: toasty_core::stmt::Value::I64(1), .. },
297                _ { name.upper_camel_case(): "Active", discriminant: toasty_core::stmt::Value::I64(2), .. },
298                _ { name.upper_camel_case(): "Done", discriminant: toasty_core::stmt::Value::I64(3), .. },
299            ],
300        }),
301    });
302
303    // Embedded enums don't create database tables (stored as a column in parent)
304    assert!(schema.db.tables.is_empty());
305}
306
307/// Tests the complete schema generation and mapping for an embedded enum field:
308/// - App schema: enum field with correct type reference
309/// - DB schema: enum field stored as a single INTEGER column
310/// - Mapping: enum field maps directly to a primitive column (discriminant IS the value)
311#[driver_test]
312pub async fn root_model_with_embedded_enum_field(test: &mut Test) {
313    #[derive(toasty::Embed)]
314    enum Status {
315        #[column(variant = 1)]
316        Pending,
317        #[column(variant = 2)]
318        Active,
319        #[column(variant = 3)]
320        Done,
321    }
322
323    #[derive(toasty::Model)]
324    struct User {
325        #[key]
326        id: String,
327        #[allow(dead_code)]
328        status: Status,
329    }
330
331    let db = test.setup_db(models!(User, Status)).await;
332    let schema = db.schema();
333
334    // Both embedded enum and root model exist in app schema
335    assert_struct!(schema.app.models, #{
336        Status::id(): toasty::schema::app::Model::EmbeddedEnum({
337            name.upper_camel_case(): "Status",
338            variants.len(): 3,
339        }),
340        User::id(): toasty::schema::app::Model::Root({
341            name.upper_camel_case(): "User",
342            fields: [
343                { name.app: Some("id") },
344                {
345                    name.app: Some("status"),
346                    ty: FieldTy::Embedded({
347                        target: == Status::id(),
348                    }),
349                },
350            ],
351        }),
352    });
353
354    // Database table has a single INTEGER column for the enum discriminant
355    assert_struct!(schema.db.tables, [
356        {
357            name: =~ r"users$",
358            columns: [
359                { name: "id" },
360                { name: "status" },
361            ],
362        },
363    ]);
364
365    let user = &schema.app.models[&User::id()];
366    let user_table = schema.table_for(user);
367    let user_mapping = &schema.mapping.models[&User::id()];
368
369    assert_struct!(user_mapping, {
370        columns.len(): 2,
371        fields: [
372            mapping::Field::Primitive(FieldPrimitive {
373                column: == user_table.columns[0].id,
374                lowering: 0,
375                ..
376            }),
377            mapping::Field::Enum(FieldEnum {
378                discriminant: FieldPrimitive {
379                    column: == user_table.columns[1].id,
380                    lowering: 1,
381                    ..
382                },
383                variants.len(): 3,
384                ..
385            }),
386        ],
387    });
388}