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