tank_tests/
books.rs

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