1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
//! # `pg-worm`
//! ### *P*ost*g*reSQL's *W*orst *ORM*
//! `pg-worm` is an opiniated, straightforward, async ORM for PostgreSQL servers.
//! Well, at least that's the goal.
//!
//! This library is based on [`tokio_postgres`](https://docs.rs/tokio-postgres/0.7.8/tokio_postgres/index.html)
//! and is intended to be used with [`tokio`](https://tokio.rs/).
//!
//! ## Usage
//! Fortunately, using this library is very easy.
//!
//! Just derive the [`Model`] trait for your type, connect to your database
//! and you are ready to go!
//!
//! Here's a quick example:
//!
//! ```
//! use pg_worm::{connect, force_register, Filter, JoinType, Model, NoTls, Query, QueryBuilder};
//! use tokio::try_join;
//! 
//! // First easily define your models.
//! #[derive(Model)]
//! struct Book {
//!     // `id` will be the primary key column and
//! 	// automatically generated/incremented
//!     #[column(primary_key, auto)]
//!     id: i64,
//!     #[column(unique)]
//!     title: String,
//!     author_id: i64,
//! }
//! 
//! #[derive(Model)]
//! struct Author {
//!     #[column(primary_key, auto)]
//!     id: i64,
//!     name: String,
//! }
//! 
//! #[tokio::main]
//! async fn main() -> Result<(), pg_worm::Error> {
//!     // First connect to your server. This can be only done once.
//!     connect!("postgres://me:me@localhost:5432", NoTls).await?;
//! 
//!     // Then, create tables for your models.
//!     // Use `register!` if you want to fail if a
//!     // table with the same name already exists.
//!     //
//!     // `force_register` drops the old table,
//!     // which is useful for development.
//!     //
//!     // If your tables already exist, skip this part.
//!     force_register!(Author, Book)?;
//! 
//!     // Next, insert some data.
//!     // This works by passing values for all
//!     // fields which aren't autogenerated.
//!     try_join!(
//!         Author::insert("Stephen King"),
//!         Author::insert("Martin Luther King"),
//!         Author::insert("Karl Marx"),
//!         Book::insert("Foo - Part I", 1),
//!         Book::insert("Foo - Part II", 2),
//!         Book::insert("Foo - Part III", 3)
//!     )?;
//! 
//! 	// Do a simple query for all books
//!     let books: Vec<Book> = Book::select(Filter::all()).await;
//!     assert_eq!(books.len(), 3);
//! 
//!     // Or search for a specific book
//!     let book = Book::select_one(Book::title.eq("Foo - Part II")).await;
//!     assert!(book.is_some());
//! 
//!     // Or make more complex queries using the query builder
//!     let king_books: Vec<Book> = Query::select(Book::COLUMNS)
//!         .filter(Author::name.like("%King%"))
//!         .join(&Book::author_id, &Author::id, JoinType::Inner)
//!         .build()
//!         .exec()
//!         .await?;
//!     assert_eq!(king_books.len(), 2);
//! 
//!     // Or delete a book, you don't like
//!     Book::delete(Book::title.eq("Foo - Part II")).await;
//! 
//! 	// Graceful shutdown
//!     Ok(())
//! }
//! ```
//!
//! ## Filters
//! [`Filter`]s can be used to easily include `WHERE` clauses in your queries.
//!
//! They can be constructed by calling functions of the respective column.
//! `pg_worm` automatically constructs a [`TypedColumn`] constant for each field
//! of your `Model`.
//!
//! A practical example would look like this:
//!
//! ```ignore
//! MyModel::select(MyModel::my_field.eq(5))
//! ```
//!
//! Currently the following filter functions are supported:
//!
//!  * `Filter::all()` - doesn't check anything
//!  * `eq(T)` - checks whether the column value is equal to something
//!  * `one_of(Vec<T>)` - checks whether the vector contains the column value.
//!  
//! You can also do filter logic using `!`, `&` and `|`: `MyModel::my_field.eq(5) & !MyModel::other_field.eq("Foo")`.
//! This works as you expect logical OR and AND to work.
//! Please notice that, at this point, custom priorization via parantheses
//! is **not possible**.
//!
//! 
//! ## Query Builder
//! Simply attaching a [`Filter`] to your query often does not suffice. 
//! For this reason, `pg-worm` provides a `QueryBuilder` interface for
//! constructing more complex queries. 
//! 
//! Start building your query by calling `Query::select()` and passing 
//! the columns you want to select. 
//! Normally you want to query all columns of a `Model` which you can do by passing 
//! `YourModel::columns()`.
//! 
//! You can modify your query using the following methods:
//! 
//!  * `.filter()` - add a `WHERE` clause
//!  * `.join()` - add a `JOIN` for querying accross tables/models
//!  * `.limit()` - add a `LIMIT` to how many rows are returned
//! 
//! After you have configured your query, build it using the `.build()` method.
//! Then, execute it by calling `.exec::<M>()`, where `M` is the `Model` which
//! should be parsed from the query result. It may be inferred.
//! 
//! ## Opiniatedness
//! As mentioned before, `pg_worm` is opiniated in a number of ways.
//! These include:
//!
//!  * `panic`s. For the sake of convenience `pg_worm` only returns a  `Result` when
//!    inserting data, since in that case Postgres might reject the data because of
//!    some constraint.
//!
//!    This means that should something go wrong, like:
//!     - the connection to the database collapsed,
//!     - `pg_worm` is unable to parse Postgres' response,
//!     - ...
//!
//!    the program will panic.
//!  * ease of use. The goal of `pg_worm` is **not** to become an enterprise solution.
//!    If adding an option means infringing the ease of use then it will likely
//!    not be added.

// This allows importing this crate's contents from pg-worm-derive.
extern crate self as pg_worm;

pub mod query;

pub use async_trait::async_trait;
pub use pg::{NoTls, Row};
pub use pg_worm_derive::Model;
/// This crate's reexport of the `tokio_postgres` crate.
pub use tokio_postgres as pg;

pub use query::*;

use once_cell::sync::OnceCell;
use pg::{tls::MakeTlsConnect, Client, Connection, Socket};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error("couldn't connect to database")]
    ConnectionError,
    #[error("already connected to database")]
    AlreadyConnected,
    #[error("not connected to database")]
    NotConnected,
    #[error("error communicating with database")]
    PostgresError(#[from] tokio_postgres::Error),
}

/// This is the trait which you should derive for your model structs.
///
/// It provides the ORM functionality.
///
#[async_trait]
pub trait Model<T>: for<'a> TryFrom<&'a Row, Error = Error> {
    /// This is a library function needed to derive the `Model`trait.
    ///
    /// *_DO NOT USE_*
    #[must_use]
    fn _table_creation_sql() -> &'static str;

    fn columns() -> &'static [&'static DynCol];

    /// Retrieve all entities from the table.
    ///
    /// # Panics
    /// For the sake of convenience this function does not return
    /// a `Result` but panics instead
    ///  - if there is no database connection
    #[must_use]
    async fn select(filter: Filter) -> Vec<T>;

    /// Retrieve the first entity from the database.
    /// Returns `None` if there are no entities present.
    ///
    /// # Panics
    /// For the sake of convenience this function does not return
    /// a `Result` but panics instead
    ///  - if there is no database connection
    #[must_use]
    async fn select_one(filter: Filter) -> Option<T>;

    /// Delete any entity wich matches the filter.
    ///
    /// Returns the number of rows affected.
    ///
    /// # Panic
    /// For the sake of convenience this function does not return
    /// a `Result` but panics instead
    ///  - if there is no database connection
    async fn delete(filter: Filter) -> u64;
}

static CLIENT: OnceCell<Client> = OnceCell::new();

/// Get a reference to the client, if a connection has been made.
/// Returns `Err(Error::NotConnected)` otherwise.
///
/// **This is a private library function needed to derive
/// the `Model` trait. Do not use!**
#[inline]
pub fn _get_client() -> Result<&'static Client, Error> {
    if let Some(client) = CLIENT.get() {
        Ok(client)
    } else {
        Err(Error::NotConnected)
    }
}

/// Connect the `pg_worm` client to a postgres database.
///
/// You need to *_activate the connection by spawning it off into a new thread_*, only then will the client actually work.
///
/// You can connect to a database only once. If you try to connect again,
/// the function will return an error.
///
/// # Example
/// ```ignore
/// let conn = connect("my_db_url", NoTls).expect("db connection failed");
/// tokio::spawn(async move {
///     conn.await.expect("connection error")
/// });
/// ```
pub async fn connect<T>(config: &str, tls: T) -> Result<Connection<Socket, T::Stream>, Error>
where
    T: MakeTlsConnect<Socket>,
{
    let (client, conn) = tokio_postgres::connect(config, tls).await?;
    match CLIENT.set(client) {
        Ok(_) => (),
        Err(_) => return Err(Error::AlreadyConnected),
    };
    Ok(conn)
}

/// Convenience macro for connecting the `pg-worm` client
/// to a database server. Essentially writes the boilerplate
/// code needed. See the [`tokio_postgres`](https://docs.rs/tokio-postgres/latest/tokio_postgres/config/struct.Config.html)
/// documentation for more information on the config format.
/// 
/// Calls the [`connect()`] function.
/// Needs `tokio` to work.
///
/// # Panics
/// Panics when the connection is closed due to a fatal error.
#[macro_export]
macro_rules! connect {
    ($config:literal, $tls:expr) => {
        async {
            match $crate::connect($config, $tls).await {
                Ok(conn) => {
                    tokio::spawn(async move { conn.await.expect("fatal connection error") });
                    return Ok(());
                }
                Err(err) => return Err(err),
            }
        }
    };
}

/// Register your model with the database.
/// This creates a table representing your model.
///
/// Use the [`register!`] macro for a more convenient api.
///
/// # Usage
/// ```ignore
/// #[derive(Model)]
/// struct Foo {
///     #[column(primary_key)]
///     id: i64
/// }
///
/// #[tokio::main]
/// async fn main() -> Result<(), pg_worm::Error> {
///     // ---- snip connection setup ----
///     pg_worm::register_model::<M>().await?;
/// }
/// ```
pub async fn register_model<M: Model<M>>() -> Result<(), Error>
where
    for<'a> Error: From<<M as TryFrom<&'a Row>>::Error>,
{
    let client = _get_client()?;
    client.batch_execute(M::_table_creation_sql()).await?;

    Ok(())
}

/// Same as [`register_model`] but if a table with the same name
/// already exists, it is dropped instead of returning an error.
pub async fn force_register_model<M: Model<M>>() -> Result<(), Error>
where
    for<'a> Error: From<<M as TryFrom<&'a Row>>::Error>,
{
    let client = _get_client()?;
    let query = format!(
        "DROP TABLE IF EXISTS {} CASCADE; ",
        M::columns()[0].table_name()
    ) + M::_table_creation_sql();

    client.batch_execute(&query).await?;

    Ok(())
}

/// Registers a [`Model`] with the database by creating a
/// corresponding table.
///
/// This is just a more convenient version api
/// for the [`register_model`] function.
///
/// This macro, too, requires the `tokio` crate.
/// 
/// Returns an error if:
///  - a table with the same name already exists,
///  - the client is not connected,
///  - the creation of the table fails
///
/// # Usage
///
/// ```ignore
/// use pg_worm::{Model, register};
///
/// #[derive(Model)]
/// struct Foo {
///     #[column(primary_key)]
///     id: i64
/// }
///
/// #[tokio::main]
/// async fn main() -> Result<(), pg_worm::Error> {
///     // ---- snip connection setup ----
///     register!(Foo)?;
/// }
/// ```
#[macro_export]
macro_rules! register {
    ($($x:ty),+) => {
        tokio::try_join!(
            $($crate::register_model::<$x>()),*
        )
    };
}

/// Like [`register!`] but if a table with the same name already
/// exists, it is dropped instead of returning an error.
#[macro_export]
macro_rules! force_register {
    ($($x:ty),+) => {
        tokio::try_join!(
            $($crate::force_register_model::<$x>()),*
        )
    };
}

#[cfg(test)]
mod tests {
    #![allow(dead_code)]

    use pg_worm::{Join, JoinType, Model, Query, QueryBuilder};

    use crate::{Filter, ToQuery};

    #[derive(Model)]
    #[table(table_name = "persons")]
    struct Person {
        #[column(primary_key, auto)]
        id: i64,
        name: String,
    }

    #[derive(Model)]
    struct Book {
        #[column(primary_key, auto)]
        id: i64,
        title: String,
        author_id: i64,
    }

    #[test]
    fn table_name() {
        assert_eq!(Book::COLUMNS[0].table_name(), "book");
    }

    #[test]
    fn join_sql() {
        assert_eq!(
            Join::new(&Book::author_id, &Person::id, JoinType::Inner).to_sql(),
            "INNER JOIN persons ON book.author_id = persons.id"
        )
    }

    #[test]
    fn select_sql() {
        let q = Query::select([&Book::title])
            .filter(Person::name.like("%a%"))
            .join(&Book::author_id, &Person::id, JoinType::Inner)
            .limit(4)
            .build();

        assert_eq!(
            q.stmt(),
            "SELECT book.title FROM book INNER JOIN persons ON book.author_id = persons.id WHERE persons.name LIKE $1 LIMIT 4"
        )
    }

    #[test]
    fn table_creation_sql() {
        assert_eq!(
            Person::_table_creation_sql(),
            "CREATE TABLE persons (id int8 PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name text)"
        );
    }

    #[test]
    fn limit_to_query() {
        let limit = Some(4usize);
        assert_eq!(limit.to_sql(), "LIMIT 4");
    }

    #[test]
    fn empty_limit_tp_query() {
        let limit: Option<usize> = None;
        assert_eq!(limit.to_sql(), "");
    }

    #[test]
    fn empty_filter_to_query() {
        let filter = Filter::all();
        assert_eq!(filter.to_sql(), "");
    }

    #[test]
    fn eq_filter_to_query() {
        let filter = Book::id.eq(5);
        assert_eq!(filter.to_sql(), "WHERE book.id = $1");
    }

    #[test]
    fn filter_and() {
        let filter = Book::id.eq(4) & Book::id.eq(5);
        assert_eq!(filter.to_sql(), "WHERE book.id = $1 AND book.id = $2");
    }
}