Skip to main content

tank_tests/
shopping.rs

1#![allow(unused_imports)]
2use rust_decimal::Decimal;
3use std::collections::HashMap;
4use std::pin::pin;
5use std::{str::FromStr, sync::Arc, sync::LazyLock};
6use tank::QueryBuilder;
7use tank::{
8    AsValue, Dataset, Entity, Executor, FixedDecimal, cols, expr, join,
9    stream::{StreamExt, TryStreamExt},
10};
11use time::{Date, Month, PrimitiveDateTime, Time};
12use tokio::sync::Mutex;
13use uuid::Uuid;
14
15static MUTEX: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
16
17#[derive(Default, Entity, Debug)]
18#[tank(schema = "shopping", primary_key = Self::id)]
19struct Product {
20    id: usize,
21    name: String,
22    price: FixedDecimal<8, 2>,
23    desc: Option<String>,
24    stock: Option<isize>,
25    #[cfg(not(feature = "disable-lists"))]
26    tags: Vec<String>,
27}
28
29#[derive(Entity, Debug)]
30#[tank(schema = "shopping", primary_key = Self::id)]
31struct User {
32    id: Uuid,
33    name: String,
34    email: String,
35    birthday: Date,
36    #[cfg(not(feature = "disable-lists"))]
37    preferences: Option<Arc<Vec<String>>>,
38    registered: PrimitiveDateTime,
39}
40
41#[derive(Entity, Debug)]
42#[tank(schema = "shopping", primary_key = (user, product))]
43struct Cart {
44    #[tank(references = User::id)]
45    user: Uuid,
46    #[tank(references = Product::id)]
47    product: usize,
48    /// The price can stay locked once added to the shopping cart
49    price: FixedDecimal<8, 2>,
50    timestamp: PrimitiveDateTime,
51}
52
53pub async fn shopping(executor: &mut impl Executor) {
54    let _lock = MUTEX.lock().await;
55
56    // Product
57    Product::drop_table(executor, true, false)
58        .await
59        .expect("Failed to drop product table");
60    Product::create_table(executor, false, true)
61        .await
62        .expect("Failed to create the product table");
63    let products = [
64        Product {
65            id: 1,
66            name: "Rust-Proof Coffee Mug".into(),
67            price: Decimal::new(12_99, 2).into(),
68            desc: Some("Keeps your coffee warm and your compiler calm.".into()),
69            stock: 42.into(),
70            #[cfg(not(feature = "disable-lists"))]
71            tags: vec!["kitchen".into(), "coffee".into(), "metal".into()].into(),
72        },
73        Product {
74            id: 2,
75            name: "Zero-Cost Abstraction Hoodie".into(),
76            price: Decimal::new(49_95, 2).into(),
77            desc: Some("For developers who think runtime overhead is a moral failure.".into()),
78            stock: 10.into(),
79            #[cfg(not(feature = "disable-lists"))]
80            tags: vec!["clothing".into(), "nerdwear".into()].into(),
81        },
82        Product {
83            id: 3,
84            name: "Thread-Safe Notebook".into(),
85            price: Decimal::new(7_50, 2).into(),
86            desc: None,
87            stock: 0.into(),
88            #[cfg(not(feature = "disable-lists"))]
89            tags: vec!["stationery".into()].into(),
90        },
91        Product {
92            id: 4,
93            name: "Async Teapot".into(),
94            price: Decimal::new(25_00, 2).into(),
95            desc: Some("Returns 418 on brew() call.".into()),
96            stock: 3.into(),
97            #[cfg(not(feature = "disable-lists"))]
98            tags: vec!["kitchen".into(), "humor".into()].into(),
99        },
100    ];
101    Product::insert_many(executor, &products)
102        .await
103        .expect("Could not insert the products");
104    let total_products = Product::find_many(executor, true, None)
105        .map_err(|e| panic!("{e:#}"))
106        .count()
107        .await;
108    assert_eq!(total_products, 4);
109    let ordered_products = executor
110        .fetch(
111            QueryBuilder::new()
112                .select([Product::id, Product::name, Product::price])
113                .from(Product::table())
114                .where_expr(expr!(Product::stock > 0))
115                .order_by(cols!(Product::price ASC))
116                .build(&executor.driver()),
117        )
118        .map(|r| r.and_then(Product::from_row))
119        .try_collect::<Vec<Product>>()
120        .await
121        .expect("Could not get the products ordered by increasing price");
122    assert!(
123        ordered_products.iter().map(|v| &v.name).eq([
124            "Rust-Proof Coffee Mug",
125            "Async Teapot",
126            "Zero-Cost Abstraction Hoodie"
127        ]
128        .into_iter())
129    );
130    let zero_stock = Product::find_one(executor, expr!(Product::stock == 0))
131        .await
132        .expect("Failed to query product with zero stock")
133        .expect("Expected a product with zero stock");
134    assert_eq!(zero_stock.id, 3);
135
136    // Decrease stock for product id 4 by 1 and verify save works
137    let mut prod4 = Product::find_one(executor, expr!(Product::id == 4))
138        .await
139        .expect("Failed to query product 4")
140        .expect("Product 4 expected");
141    let old_stock = prod4.stock.unwrap_or(0);
142    prod4.stock = Some(old_stock - 1);
143    prod4
144        .save(executor)
145        .await
146        .expect("Failed to save updated product 4");
147    let prod4_after = Product::find_one(executor, expr!(Product::id == 4))
148        .await
149        .expect("Failed to query product 4 after update")
150        .expect("Product 4 expected after update");
151    assert_eq!(prod4_after.stock, Some(old_stock - 1));
152
153    // User
154    User::drop_table(executor, true, false)
155        .await
156        .expect("Failed to drop user table");
157    User::create_table(executor, false, false)
158        .await
159        .expect("Failed to create the user table");
160    let users = vec![
161        User {
162            id: Uuid::new_v4(),
163            name: "Alice Compiler".into(),
164            email: "alice@example.com".into(),
165            birthday: Date::from_calendar_date(1995, Month::May, 17).unwrap(),
166            #[cfg(not(feature = "disable-lists"))]
167            preferences: Some(vec!["dark_mode".into(), "express_shipping".into()].into()),
168            registered: PrimitiveDateTime::new(
169                Date::from_calendar_date(2023, Month::January, 2).unwrap(),
170                Time::from_hms(10, 30, 0).unwrap(),
171            ),
172        },
173        User {
174            id: Uuid::new_v4(),
175            name: "Bob Segfault".into(),
176            email: "bob@crashmail.net".into(),
177            birthday: Date::from_calendar_date(1988, Month::March, 12).unwrap(),
178            #[cfg(not(feature = "disable-lists"))]
179            preferences: None,
180            registered: PrimitiveDateTime::new(
181                Date::from_calendar_date(2024, Month::June, 8).unwrap(),
182                Time::from_hms(22, 15, 0).unwrap(),
183            ),
184        },
185    ];
186    User::insert_many(executor, &users)
187        .await
188        .expect("Could not insert the users");
189    let row = pin!(
190        executor.fetch(
191            QueryBuilder::new()
192                .select(cols!(COUNT(*)))
193                .from(User::table())
194                .where_expr(true)
195                .limit(Some(1))
196                .build(&executor.driver())
197        )
198    )
199    .try_next()
200    .await
201    .expect("Failed to query for count")
202    .expect("Did not return some value");
203    assert_eq!(i64::try_from_value(row.values[0].clone()).unwrap(), 2);
204
205    // Cart
206    Cart::drop_table(executor, true, false)
207        .await
208        .expect("Failed to drop cart table");
209    Cart::create_table(executor, false, false)
210        .await
211        .expect("Failed to create the cart table");
212    let carts = vec![
213        Cart {
214            user: users[0].id,
215            product: 1,
216            price: Decimal::new(12_99, 2).into(),
217            timestamp: PrimitiveDateTime::new(
218                Date::from_calendar_date(2025, Month::March, 1).unwrap(),
219                Time::from_hms(9, 0, 0).unwrap(),
220            ),
221        },
222        Cart {
223            user: users[0].id,
224            product: 2,
225            price: Decimal::new(49_95, 2).into(),
226            timestamp: PrimitiveDateTime::new(
227                Date::from_calendar_date(2025, Month::March, 1).unwrap(),
228                Time::from_hms(9, 5, 0).unwrap(),
229            ),
230        },
231        Cart {
232            user: users[1].id,
233            product: 4,
234            price: Decimal::new(23_50, 2).into(),
235            timestamp: PrimitiveDateTime::new(
236                Date::from_calendar_date(2025, Month::March, 3).unwrap(),
237                Time::from_hms(14, 12, 0).unwrap(),
238            ),
239        },
240    ];
241    Cart::insert_many(executor, &carts)
242        .await
243        .expect("Could not insert the carts");
244    let cart_count = Cart::find_many(executor, true, None)
245        .map_err(|e| panic!("{e:#}"))
246        .count()
247        .await;
248    assert_eq!(cart_count, 3);
249
250    // Product 4 in cart has different price than current product price
251    let cart_for_4 = Cart::find_one(executor, expr!(Cart::product == 4))
252        .await
253        .expect("Failed to query cart for product 4");
254    let cart_for_4 = cart_for_4.expect("Expected a cart for product 4");
255    let product4 = Product::find_one(executor, expr!(Product::id == 4))
256        .await
257        .expect("Failed to query product 4 for price check")
258        .expect("Expected product 4");
259    assert_eq!(cart_for_4.price.0, Decimal::new(23_50, 2));
260    assert_eq!(product4.price.0, Decimal::new(25_00, 2));
261
262    // Delete the cart containing product 2
263    let cart_for_2 = Cart::find_one(executor, expr!(Cart::product == 2))
264        .await
265        .expect("Failed to query cart for product 2")
266        .expect("Expected a cart for product 2");
267    cart_for_2
268        .delete(executor)
269        .await
270        .expect("Failed to delete cart for product 2");
271    let cart_count_after = Cart::find_many(executor, true, None)
272        .map_err(|e| panic!("{e:#}"))
273        .count()
274        .await;
275    assert_eq!(cart_count_after, 2);
276
277    #[cfg(not(feature = "disable-joins"))]
278    {
279        #[derive(Entity, PartialEq, Debug)]
280        struct Carts {
281            user: String,
282            product: String,
283            price: Decimal,
284        }
285        let carts: Vec<Carts> = executor
286            .fetch(
287                QueryBuilder::new()
288                    .select(cols!(
289                        Product::name as product,
290                        User::name as user,
291                        Cart::price
292                    ))
293                    .from(join!(
294                        User INNER JOIN Cart ON User::id == Cart::user
295                            JOIN Product ON Cart::product == Product::id
296                    ))
297                    .where_expr(true)
298                    .order_by(cols!(Product::name ASC, User::name ASC))
299                    .build(&executor.driver()),
300            )
301            .map_ok(Carts::from_row)
302            .map(Result::flatten)
303            .try_collect::<Vec<_>>()
304            .await
305            .expect("Could not get the products ordered by increasing price");
306        assert_eq!(
307            carts,
308            &[
309                Carts {
310                    user: "Bob Segfault".into(),
311                    product: "Async Teapot".into(),
312                    price: Decimal::new(23_50, 2),
313                },
314                Carts {
315                    user: "Alice Compiler".into(),
316                    product: "Rust-Proof Coffee Mug".into(),
317                    price: Decimal::new(12_99, 2),
318                },
319            ]
320        )
321    }
322}