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)).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: None,
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), scenario(crate::scenarios::task_name_status))]
136pub async fn filter_by_enum_variant(t: &mut Test) -> Result<()> {
137    let mut db = setup(t).await;
138
139    // Create tasks with different statuses: 1 pending, 2 active, 1 done
140    for (name, status) in [
141        ("Task A", Status::Pending),
142        ("Task B", Status::Active),
143        ("Task C", Status::Active),
144        ("Task D", Status::Done),
145    ] {
146        Task::create()
147            .name(name)
148            .status(status)
149            .exec(&mut db)
150            .await?;
151    }
152
153    let status_col = column(&db, "tasks", "status");
154    t.log().clear();
155
156    // Filter: only Active tasks (discriminant = 2)
157    let active = Task::filter(Task::fields().status().eq(Status::Active))
158        .exec(&mut db)
159        .await?;
160    assert_eq!(active.len(), 2);
161    {
162        let (op, _) = t.log().pop();
163        if t.capability().sql {
164            assert_struct!(op, Operation::QuerySql({
165                stmt: Statement::Query({
166                    body: ExprSet::Select({
167                        filter.expr: Some(Expr::BinaryOp({
168                            lhs.as_expr_column_unwrap().column: == status_col.index,
169                            op: BinaryOp::Eq,
170                            *rhs: Expr::Arg({ position: 0 }),
171                        })),
172                    }),
173                }),
174                params: [{ value: == 2i64 }],
175            }));
176        } else {
177            assert_struct!(op, Operation::Scan({
178                filter: Some(Expr::BinaryOp({
179                    lhs.as_expr_column_unwrap().column: == status_col.index,
180                    op: BinaryOp::Eq,
181                    *rhs: Expr::Value(== Value::I64(2)),
182                })),
183            }));
184        }
185    }
186
187    // Filter: only Pending tasks (discriminant = 1)
188    let pending = Task::filter(Task::fields().status().eq(Status::Pending))
189        .exec(&mut db)
190        .await?;
191    assert_eq!(pending.len(), 1);
192    assert_eq!(pending[0].name, "Task A");
193    {
194        let (op, _) = t.log().pop();
195        if t.capability().sql {
196            assert_struct!(op, Operation::QuerySql({
197                stmt: Statement::Query({
198                    body: ExprSet::Select({
199                        filter.expr: Some(Expr::BinaryOp({
200                            lhs.as_expr_column_unwrap().column: == status_col.index,
201                            op: BinaryOp::Eq,
202                            *rhs: Expr::Arg({ position: 0 }),
203                        })),
204                    }),
205                }),
206                params: [{ value: == 1i64 }],
207            }));
208        } else {
209            assert_struct!(op, Operation::Scan({
210                filter: Some(Expr::BinaryOp({
211                    lhs.as_expr_column_unwrap().column: == status_col.index,
212                    op: BinaryOp::Eq,
213                    *rhs: Expr::Value(== Value::I64(1)),
214                })),
215            }));
216        }
217    }
218
219    // Filter: only Done tasks (discriminant = 3)
220    let done = Task::filter(Task::fields().status().eq(Status::Done))
221        .exec(&mut db)
222        .await?;
223    assert_eq!(done.len(), 1);
224    assert_eq!(done[0].name, "Task D");
225    {
226        let (op, _) = t.log().pop();
227        if t.capability().sql {
228            assert_struct!(op, Operation::QuerySql({
229                stmt: Statement::Query({
230                    body: ExprSet::Select({
231                        filter.expr: Some(Expr::BinaryOp({
232                            lhs.as_expr_column_unwrap().column: == status_col.index,
233                            op: BinaryOp::Eq,
234                            *rhs: Expr::Arg({ position: 0 }),
235                        })),
236                    }),
237                }),
238                params: [{ value: == 3i64 }],
239            }));
240        } else {
241            assert_struct!(op, Operation::Scan({
242                filter: Some(Expr::BinaryOp({
243                    lhs.as_expr_column_unwrap().column: == status_col.index,
244                    op: BinaryOp::Eq,
245                    *rhs: Expr::Value(== Value::I64(3)),
246                })),
247            }));
248        }
249    }
250
251    Ok(())
252}
253
254/// Tests that embedded enums are registered in the app schema but don't create
255/// their own database tables (they're inlined into parent models as a single column).
256#[driver_test(scenario(crate::scenarios::user_with_status))]
257pub async fn basic_embedded_enum(test: &mut Test) {
258    let db = setup(test).await;
259    let schema = db.schema();
260
261    // Embedded enums exist in app schema as Model::EmbeddedEnum
262    let status = &schema.app.models[&Status::id()];
263    assert_struct!(status, toasty::schema::app::Model::EmbeddedEnum({
264        name.upper_camel_case(): "Status",
265        variants: [
266            _ { name.upper_camel_case(): "Pending", discriminant: toasty_core::stmt::Value::I64(1), .. },
267            _ { name.upper_camel_case(): "Active", discriminant: toasty_core::stmt::Value::I64(2), .. },
268            _ { name.upper_camel_case(): "Done", discriminant: toasty_core::stmt::Value::I64(3), .. },
269        ],
270    }));
271}
272
273/// Tests the complete schema generation and mapping for an embedded enum field:
274/// - App schema: enum field with correct type reference
275/// - DB schema: enum field stored as a single INTEGER column
276/// - Mapping: enum field maps directly to a primitive column (discriminant IS the value)
277#[driver_test(scenario(crate::scenarios::user_with_status))]
278pub async fn root_model_with_embedded_enum_field(test: &mut Test) {
279    let db = setup(test).await;
280    let schema = db.schema();
281
282    // Both embedded enum and root model exist in app schema
283    assert_struct!(schema.app.models, #{
284        Status::id(): toasty::schema::app::Model::EmbeddedEnum({
285            name.upper_camel_case(): "Status",
286            variants.len(): 3,
287        }),
288        User::id(): toasty::schema::app::Model::Root({
289            name.upper_camel_case(): "User",
290            fields: [
291                { name.app: Some("id") },
292                {
293                    name.app: Some("status"),
294                    ty: FieldTy::Embedded({
295                        target: == Status::id(),
296                    }),
297                },
298            ],
299        }),
300    });
301
302    // Database table has a single INTEGER column for the enum discriminant
303    assert_struct!(schema.db.tables, [
304        {
305            name: =~ r"users$",
306            columns: [
307                { name: "id" },
308                { name: "status" },
309            ],
310        },
311    ]);
312
313    let user = &schema.app.models[&User::id()];
314    let user_table = schema.table_for(user);
315    let user_mapping = &schema.mapping.models[&User::id()];
316
317    assert_struct!(user_mapping, {
318        columns.len(): 2,
319        fields: [
320            mapping::Field::Primitive(FieldPrimitive {
321                column: == user_table.columns[0].id,
322                lowering: 0,
323                ..
324            }),
325            mapping::Field::Enum(FieldEnum {
326                discriminant: FieldPrimitive {
327                    column: == user_table.columns[1].id,
328                    lowering: 1,
329                    ..
330                },
331                variants.len(): 3,
332                ..
333            }),
334        ],
335    });
336}