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