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