Skip to main content

tank_tests/
books.rs

1#![allow(unused_imports)]
2use std::{collections::HashSet, pin::pin, sync::LazyLock};
3use tank::{
4    DynQuery, AsValue, Dataset, Driver, Entity, Executor, Passive, Query, QueryBuilder, QueryResult,
5    RowLabeled, SqlWriter, Value, cols, expr, join, stream::{StreamExt, TryStreamExt}
6};
7use tokio::sync::Mutex;
8use uuid::Uuid;
9
10static MUTEX: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
11
12#[derive(Entity, Debug, Clone, PartialEq)]
13#[tank(schema = "testing", name = "authors")]
14pub struct Author {
15    #[tank(primary_key, name = "author_id")]
16    pub id: Passive<Uuid>,
17    pub name: String,
18    pub country: String,
19    pub books_published: Option<u16>,
20}
21
22#[derive(Entity, Debug, Clone, PartialEq)]
23#[tank(schema = "testing", name = "books", primary_key = (Self::title, Self::author))]
24pub struct Book {
25    #[cfg(not(feature = "disable-arrays"))]
26    pub isbn: [u8; 13],
27    #[tank(column_type = (mysql = "VARCHAR(255)"))]
28    pub title: String,
29    /// Main author
30    #[tank(references = Author::id)]
31    pub author: Uuid,
32    #[tank(references = Author::id)]
33    pub co_author: Option<Uuid>,
34    pub year: i32,
35}
36
37pub async fn books<E: Executor>(executor: &mut E) {
38    let _lock = MUTEX.lock().await;
39
40    // Setup
41    Book::drop_table(executor, true, false)
42        .await
43        .expect("Failed to drop Book table");
44    Author::drop_table(executor, true, false)
45        .await
46        .expect("Failed to drop Author table");
47    Author::create_table(executor, false, true)
48        .await
49        .expect("Failed to create Author table");
50    Book::create_table(executor, false, true)
51        .await
52        .expect("Failed to create Book table");
53
54    // Author objects
55    let authors = vec![
56        Author {
57            id: Uuid::parse_str("f938f818-0a40-4ce3-8fbc-259ac252a1b5")
58                .unwrap()
59                .into(),
60            name: "J.K. Rowling".into(),
61            country: "UK".into(),
62            books_published: 24.into(),
63        },
64        Author {
65            id: Uuid::parse_str("a73bc06a-ff89-44b9-a62f-416ebe976285")
66                .unwrap()
67                .into(),
68            name: "J.R.R. Tolkien".into(),
69            country: "USA".into(),
70            books_published: 6.into(),
71        },
72        Author {
73            id: Uuid::parse_str("6b2f56a1-316d-42b9-a8ba-baca42c5416c")
74                .unwrap()
75                .into(),
76            name: "Dmitrij Gluchovskij".into(),
77            country: "Russia".into(),
78            books_published: 7.into(),
79        },
80        Author {
81            id: Uuid::parse_str("d3d3d3d3-d3d3-d3d3-d3d3-d3d3d3d3d3d3")
82                .unwrap()
83                .into(),
84            name: "Linus Torvalds".into(),
85            country: "Finland".into(),
86            books_published: None,
87        },
88    ];
89    let rowling_id = authors[0].id.clone().unwrap();
90    let tolkien_id = authors[1].id.clone().unwrap();
91    let gluchovskij_id = authors[2].id.clone().unwrap();
92
93    // Book objects
94    let books = vec![
95        Book {
96            #[cfg(not(feature = "disable-arrays"))]
97            isbn: [9, 7, 8, 0, 7, 4, 7, 5, 3, 2, 6, 9, 9],
98            title: "Harry Potter and the Philosopher's Stone".into(),
99            author: rowling_id,
100            co_author: None,
101            year: 1937,
102        },
103        Book {
104            #[cfg(not(feature = "disable-arrays"))]
105            isbn: [9, 7, 8, 0, 7, 4, 7, 5, 9, 1, 0, 5, 4],
106            title: "Harry Potter and the Deathly Hallows".into(),
107            author: rowling_id,
108            co_author: None,
109            year: 2007,
110        },
111        Book {
112            #[cfg(not(feature = "disable-arrays"))]
113            isbn: [9, 7, 8, 0, 6, 1, 8, 2, 6, 0, 3, 0, 0],
114            title: "The Hobbit".into(),
115            author: tolkien_id,
116            co_author: None,
117            year: 1996,
118        },
119        Book {
120            #[cfg(not(feature = "disable-arrays"))]
121            isbn: [9, 7, 8, 5, 1, 7, 0, 5, 9, 6, 7, 8, 2],
122            title: "Metro 2033".into(),
123            author: gluchovskij_id,
124            co_author: None,
125            year: 2002,
126        },
127        Book {
128            #[cfg(not(feature = "disable-arrays"))]
129            isbn: [9, 7, 8, 0, 0, 2, 3, 4, 5, 6, 7, 8, 9],
130            title: "Hogwarts 2033".into(),
131            author: rowling_id,
132            co_author: gluchovskij_id.into(),
133            year: 2026,
134        },
135    ];
136
137    // Insert
138    let result = Author::insert_many(executor, authors.iter())
139        .await
140        .expect("Failed to insert authors");
141    if let Some(affected) = result.rows_affected {
142        assert_eq!(affected, 4);
143    }
144    let result = Book::insert_many(executor, books.iter())
145        .await
146        .expect("Failed to insert books");
147    if let Some(affected) = result.rows_affected {
148        assert_eq!(affected, 5);
149    }
150
151    // Find authors
152    let id = Uuid::parse_str("f938f818-0a40-4ce3-8fbc-259ac252a1b5").unwrap().as_value();
153    let author = Author::find_one(
154        executor,
155        expr!(Author::id == #id)
156    )
157        .await
158        .expect("Failed to query author by pk");
159    assert_eq!(
160        author,
161        Some(Author {
162            id: Uuid::parse_str("f938f818-0a40-4ce3-8fbc-259ac252a1b5")
163                .unwrap()
164                .into(),
165            name: "J.K. Rowling".into(),
166            country: "UK".into(),
167            books_published: 24.into(),
168        })
169    );
170
171    let author = Author::find_one(executor, expr!(Author::name == "Linus Torvalds"))
172        .await
173        .expect("Failed to query author by pk");
174    assert_eq!(
175        author,
176        Some(Author {
177            id: Uuid::parse_str("d3d3d3d3-d3d3-d3d3-d3d3-d3d3d3d3d3d3")
178                .unwrap()
179                .into(),
180            name: "Linus Torvalds".into(),
181            country: "Finland".into(),
182            books_published: None,
183        })
184    );
185
186    // Get books before 2000
187    #[cfg(not(feature = "disable-multiple-statements"))]
188    {
189        let result = executor
190            .fetch(
191                QueryBuilder::new()
192                    .select(cols!(B.title, A.name))
193                    .from(join!(Book B JOIN Author A ON B.author == A.author_id))
194                    .where_expr(expr!(B.year < 2000))
195                    .build(&executor.driver()),
196            )
197            .try_collect::<Vec<RowLabeled>>()
198            .await
199            .expect("Failed to query books and authors joined")
200            .into_iter()
201            .map(|row| {
202                let mut iter = row.values.into_iter();
203                (
204                    match iter.next().unwrap() {
205                        Value::Varchar(Some(v)) => v,
206                        Value::Unknown(Some(v)) => v.into(),
207                        v => panic!("Expected first value to be non null varchar, found {v:?}"),
208                    },
209                    match iter.next().unwrap() {
210                        Value::Varchar(Some(v)) => v,
211                        Value::Unknown(Some(v)) => v.into(),
212                        v => panic!("Expected second value to be non null varchar, found {v:?}"),
213                    },
214                )
215            })
216            .collect::<HashSet<_>>();
217        assert_eq!(
218            result,
219            HashSet::from_iter([
220                (
221                    "Harry Potter and the Philosopher's Stone".into(),
222                    "J.K. Rowling".into()
223                ),
224                ("The Hobbit".into(), "J.R.R. Tolkien".into()),
225            ])
226        );
227
228        // Get all books with their authors
229        let dataset = join!(
230            Book B LEFT JOIN Author A1 ON B.author == A1.author_id
231                LEFT JOIN Author A2 ON B.co_author == A2.author_id
232        );
233        let result = executor.fetch(
234                QueryBuilder::new()
235                    .select(cols!(B.title, A1.name as author, A2.name as co_author))
236                    .from(dataset)
237                    .where_expr(true)
238                    .build(&executor.driver())
239            ) 
240            .try_collect::<Vec<RowLabeled>>()
241            .await
242            .expect("Failed to query books and authors joined")
243            .into_iter()
244            .map(|row| {
245                let mut iter = row.values.into_iter();
246                (
247                    match iter.next().unwrap() {
248                        Value::Varchar(Some(v)) => v,
249                        Value::Unknown(Some(v)) => v.into(),
250                        v => panic!("Expected 1st value to be non null varchar, found {v:?}"),
251                    },
252                    match iter.next().unwrap() {
253                        Value::Varchar(Some(v)) => v,
254                        Value::Unknown(Some(v)) => v.into(),
255                        v => panic!("Expected 2nd value to be non null varchar, found {v:?}"),
256                    },
257                    match iter.next().unwrap() {
258                        Value::Varchar(Some(v)) => Some(v),
259                        Value::Unknown(Some(v)) => Some(v.into()),
260                        Value::Varchar(None) | Value::Null => None,
261                        v => panic!(
262                            "Expected 3rd value to be a Some(Value::Varchar(..)) | Value::Unknown(Some(..)) | Some(Value::Null)), found {v:?}",
263                        ),
264                    },
265                )
266            })
267            .collect::<HashSet<_>>();
268        assert_eq!(
269            result,
270            HashSet::from_iter([
271                (
272                    "Harry Potter and the Philosopher's Stone".into(),
273                    "J.K. Rowling".into(),
274                    None
275                ),
276                (
277                    "Harry Potter and the Deathly Hallows".into(),
278                    "J.K. Rowling".into(),
279                    None
280                ),
281                ("The Hobbit".into(), "J.R.R. Tolkien".into(), None),
282                ("Metro 2033".into(), "Dmitrij Gluchovskij".into(), None),
283                (
284                    "Hogwarts 2033".into(),
285                    "J.K. Rowling".into(),
286                    Some("Dmitrij Gluchovskij".into())
287                ),
288            ])
289        );
290
291        // Get book and author pairs
292        #[derive(Debug, Entity, PartialEq, Eq, Hash)]
293        struct Books {
294            pub title: Option<String>,
295            pub author: Option<String>,
296        }
297        let books = executor.fetch(
298                QueryBuilder::new()
299                    .select(cols!(Book::title, Author::name as author, Book::year))
300                    .from(join!(Book JOIN Author ON Book::author == Author::id))
301                    .where_expr(true)
302                    .build(&executor.driver())
303            )
304            .and_then(|row| async { Books::from_row(row) })
305            .try_collect::<HashSet<_>>()
306            .await
307            .expect("Could not return the books");
308        assert_eq!(
309            books,
310            HashSet::from_iter([
311                Books {
312                    title: Some("Harry Potter and the Philosopher's Stone".into()),
313                    author: Some("J.K. Rowling".into())
314                },
315                Books {
316                    title: Some("Harry Potter and the Deathly Hallows".into()),
317                    author: Some("J.K. Rowling".into())
318                },
319                Books {
320                    title: Some("The Hobbit".into()),
321                    author: Some("J.R.R. Tolkien".into())
322                },
323                Books {
324                    title: Some("Metro 2033".into()),
325                    author: Some("Dmitrij Gluchovskij".into())
326                },
327                Books {
328                    title: Some("Hogwarts 2033".into()),
329                    author: Some("J.K. Rowling".into())
330                },
331            ])
332        );
333
334        #[cfg(not(feature = "disable-references"))]
335        {
336            // Insert book violating referential integrity
337            use crate::silent_logs;
338            let book = Book {
339                #[cfg(not(feature = "disable-arrays"))]
340                isbn: [9, 7, 8, 1, 7, 3, 3, 5, 6, 1, 0, 8, 0],
341                title: "My book".into(),
342                author: Uuid::parse_str("c18c04b4-1aae-48a3-9814-9b70f7a38315").unwrap(),
343                co_author: None,
344                year: 2025,
345            };
346            silent_logs! {
347                assert!(
348                    book.save(executor).await.is_err(),
349                    "Must fail to save book violating referential integrity"
350                );
351            }
352        }
353    }
354
355    #[cfg(not(feature = "disable-ordering"))]
356    {
357        // Authors names alphabetical order
358        let authors = executor.fetch(
359            QueryBuilder::new()
360                .select([Author::name])
361                .from(Author::table())
362                .where_expr(true)
363                .order_by(cols!(Author::name ASC))
364                .build(&executor.driver())
365            )
366            .and_then(|row| async move { AsValue::try_from_value((*row.values)[0].clone()) })
367            .try_collect::<Vec<String>>()
368            .await
369            .expect("Could not return the ordered names of the authors");
370        assert_eq!(
371            authors,
372            vec![
373                "Dmitrij Gluchovskij".to_string(),
374                "J.K. Rowling".to_string(),
375                "J.R.R. Tolkien".to_string(),
376                "Linus Torvalds".to_string(),
377            ]
378        )
379    }
380
381    // Multiple statements
382    #[cfg(not(feature = "disable-multiple-statements"))]
383    {
384        let mut query = DynQuery::default();
385        let writer = executor.driver().sql_writer();
386        writer.write_select(
387            &mut query,
388            &QueryBuilder::new()
389                .select(Book::columns())
390                .from(Book::table())
391                .where_expr(expr!(Book::title == "Metro 2033"))
392                .limit(Some(1))
393        );
394        writer.write_select(
395            &mut query,
396            &QueryBuilder::new()
397                .select(Book::columns())
398                .from(Book::table())
399                .where_expr(expr!(Book::title == "Harry Potter and the Deathly Hallows"))
400                .limit(Some(1))
401        );
402        let mut stream = pin!(executor.run(query));
403        let Some(Ok(QueryResult::Row(row))) = stream.next().await else {
404            panic!("Could not get the first row")
405        };
406        let book = Book::from_row(row).expect("Could not get the book from row");
407        assert_eq!(
408            book,
409            Book {
410                #[cfg(not(feature = "disable-arrays"))]
411                isbn: [9, 7, 8, 5, 1, 7, 0, 5, 9, 6, 7, 8, 2],
412                title: "Metro 2033".into(),
413                author: gluchovskij_id,
414                co_author: None,
415                year: 2002,
416            }
417        );
418        let Some(Ok(QueryResult::Row(row))) = stream.next().await else {
419            panic!("Could not get the second row")
420        };
421        let book = Book::from_row(row).expect("Could not get the book from row");
422        assert_eq!(
423            book,
424            Book {
425                #[cfg(not(feature = "disable-arrays"))]
426                isbn: [9, 7, 8, 0, 7, 4, 7, 5, 9, 1, 0, 5, 4],
427                title: "Harry Potter and the Deathly Hallows".into(),
428                author: rowling_id,
429                co_author: None,
430                year: 2007,
431            }
432        );
433        assert!(
434            stream.next().await.is_none(),
435            "The stream should return only two rows"
436        )
437    }
438}