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    assert_struct!(t.log().pop_op(), Operation::QuerySql({
53        stmt: Statement::Insert({
54            source.body: ExprSet::Values({
55                rows: [== (Any, Any, 1i64)],
56            }),
57            target: toasty_core::stmt::InsertTarget::Table({
58                table: == user_table,
59                columns: == columns(&db, "users", &["id", "name", "status"]),
60            }),
61        }),
62    }));
63
64    // Read: discriminant is loaded back and converted to the enum variant
65    let found = User::get_by_id(&mut db, &user.id).await?;
66    assert_eq!(found.status, Status::Pending);
67
68    // Update (instance): replace the enum variant
69    t.log().clear();
70    user.update().status(Status::Active).exec(&mut db).await?;
71
72    // Verify the status column receives the new discriminant as I64
73    // Column index 2 is "status"; value I64(2) = Active discriminant
74    if t.capability().sql {
75        assert_struct!(t.log().pop_op(), Operation::QuerySql({
76            stmt: Statement::Update({
77                target: toasty_core::stmt::UpdateTarget::Table(== user_table),
78                assignments: #{ [2]: Assignment::Set(== 2i64)},
79            }),
80        }));
81    } else {
82        assert_struct!(t.log().pop_op(), Operation::UpdateByKey({
83            table: == user_table,
84            filter: None,
85            keys: _,
86            assignments: #{ [2]: Assignment::Set(== 2i64)},
87            returning: false,
88        }));
89    }
90
91    let found = User::get_by_id(&mut db, &user.id).await?;
92    assert_eq!(found.status, Status::Active);
93
94    // Update (query-based): same replacement via filter builder
95    User::filter_by_id(user.id)
96        .update()
97        .status(Status::Done)
98        .exec(&mut db)
99        .await?;
100
101    let found = User::get_by_id(&mut db, &user.id).await?;
102    assert_eq!(found.status, Status::Done);
103
104    // Delete: cleanup
105    let id = user.id;
106    user.delete().exec(&mut db).await?;
107    assert_err!(User::get_by_id(&mut db, &id).await);
108    Ok(())
109}
110
111/// Tests filtering records by embedded enum variant.
112/// SQL-only: DynamoDB requires a partition key in queries.
113/// Validates that enum fields can be used in WHERE clauses (comparing discriminants),
114/// and verifies the driver-level representation: the WHERE clause compares the status
115/// column to an I64 discriminant, not a string or other type.
116#[driver_test(requires(sql))]
117pub async fn filter_by_enum_variant(t: &mut Test) -> Result<()> {
118    #[derive(Debug, PartialEq, toasty::Embed)]
119    enum Status {
120        #[column(variant = 1)]
121        Pending,
122        #[column(variant = 2)]
123        Active,
124        #[column(variant = 3)]
125        Done,
126    }
127
128    #[derive(Debug, toasty::Model)]
129    #[allow(dead_code)]
130    struct Task {
131        #[key]
132        #[auto]
133        id: uuid::Uuid,
134        name: String,
135        status: Status,
136    }
137
138    let mut db = t.setup_db(models!(Task, Status)).await;
139
140    // Create tasks with different statuses: 1 pending, 2 active, 1 done
141    for (name, status) in [
142        ("Task A", Status::Pending),
143        ("Task B", Status::Active),
144        ("Task C", Status::Active),
145        ("Task D", Status::Done),
146    ] {
147        Task::create()
148            .name(name)
149            .status(status)
150            .exec(&mut db)
151            .await?;
152    }
153
154    let status_col = column(&db, "tasks", "status");
155    t.log().clear();
156
157    // Filter: only Active tasks (discriminant = 2)
158    let active = Task::filter(Task::fields().status().eq(Status::Active))
159        .exec(&mut db)
160        .await?;
161    assert_eq!(active.len(), 2);
162    {
163        let (op, _) = t.log().pop();
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: == 2i64,
171                    })),
172                }),
173            }),
174        }));
175    }
176
177    // Filter: only Pending tasks (discriminant = 1)
178    let pending = Task::filter(Task::fields().status().eq(Status::Pending))
179        .exec(&mut db)
180        .await?;
181    assert_eq!(pending.len(), 1);
182    assert_eq!(pending[0].name, "Task A");
183    {
184        let (op, _) = t.log().pop();
185        assert_struct!(op, Operation::QuerySql({
186            stmt: Statement::Query({
187                body: ExprSet::Select({
188                    filter.expr: Some(Expr::BinaryOp({
189                        lhs.as_expr_column_unwrap().column: == status_col.index,
190                        op: BinaryOp::Eq,
191                        *rhs: == 1i64,
192                    })),
193                }),
194            }),
195        }));
196    }
197
198    // Filter: only Done tasks (discriminant = 3)
199    let done = Task::filter(Task::fields().status().eq(Status::Done))
200        .exec(&mut db)
201        .await?;
202    assert_eq!(done.len(), 1);
203    assert_eq!(done[0].name, "Task D");
204    {
205        let (op, _) = t.log().pop();
206        assert_struct!(op, Operation::QuerySql({
207            stmt: Statement::Query({
208                body: ExprSet::Select({
209                    filter.expr: Some(Expr::BinaryOp({
210                        lhs.as_expr_column_unwrap().column: == status_col.index,
211                        op: BinaryOp::Eq,
212                        *rhs: == 3i64,
213                    })),
214                }),
215            }),
216        }));
217    }
218
219    Ok(())
220}
221
222/// Tests that embedded enums are registered in the app schema but don't create
223/// their own database tables (they're inlined into parent models as a single column).
224#[driver_test]
225pub async fn basic_embedded_enum(test: &mut Test) {
226    #[derive(toasty::Embed)]
227    enum Status {
228        #[column(variant = 1)]
229        Pending,
230        #[column(variant = 2)]
231        Active,
232        #[column(variant = 3)]
233        Done,
234    }
235
236    let db = test.setup_db(models!(Status)).await;
237    let schema = db.schema();
238
239    // Embedded enums exist in app schema as Model::EmbeddedEnum
240    assert_struct!(schema.app.models, #{
241        Status::id(): toasty::schema::app::Model::EmbeddedEnum({
242            name.upper_camel_case(): "Status",
243            variants: [
244                _ { name.upper_camel_case(): "Pending", discriminant: toasty_core::stmt::Value::I64(1), .. },
245                _ { name.upper_camel_case(): "Active", discriminant: toasty_core::stmt::Value::I64(2), .. },
246                _ { name.upper_camel_case(): "Done", discriminant: toasty_core::stmt::Value::I64(3), .. },
247            ],
248        }),
249    });
250
251    // Embedded enums don't create database tables (stored as a column in parent)
252    assert!(schema.db.tables.is_empty());
253}
254
255/// Tests the complete schema generation and mapping for an embedded enum field:
256/// - App schema: enum field with correct type reference
257/// - DB schema: enum field stored as a single INTEGER column
258/// - Mapping: enum field maps directly to a primitive column (discriminant IS the value)
259#[driver_test]
260pub async fn root_model_with_embedded_enum_field(test: &mut Test) {
261    #[derive(toasty::Embed)]
262    enum Status {
263        #[column(variant = 1)]
264        Pending,
265        #[column(variant = 2)]
266        Active,
267        #[column(variant = 3)]
268        Done,
269    }
270
271    #[derive(toasty::Model)]
272    struct User {
273        #[key]
274        id: String,
275        #[allow(dead_code)]
276        status: Status,
277    }
278
279    let db = test.setup_db(models!(User, Status)).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}