Skip to main content

tank_tests/
books.rs

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