pg_worm/lib.rs
1/*!
2# `pg-worm`
3[](https://crates.io/crates/pg-worm)
4
5[](https://docs.rs/pg-worm)
6[](https://opensource.org/licenses/MIT)
7
8### *P*ost*g*reSQL's *W*orst *ORM*
9`pg-worm` is a straightforward, fully typed, async ORM and Query Builder for PostgreSQL.
10Well, at least that's the goal.
11
12## Features/Why `pg-worm`?
13
14- Existing ORMs are not **`async`**, require you to write migrations or use a cli.
15`pg-worm`'s explicit goal is to be **easy** and to require **no setup** beyond defining your types.
16
17- `pg-worm` also features **built-in pooling** and a **concise syntax**.
18
19- `pg-worm` **doesn't get in your way** - easily include raw queries while still profiting off the other features.
20
21## Usage
22This 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/).
23
24Fortunately, using `pg-worm` is very easy.
25
26Simply derive the `Model` trait for your type, connect to your database
27and you are ready to go!
28
29Here's a quick example:
30
31```rust
32// Import the prelude to get started quickly
33use pg_worm::prelude::*;
34
35#[derive(Model)]
36struct Book {
37 // An auto-generated primary key
38 #[column(primary_key, auto)]
39 id: i64,
40 title: String,
41 author_id: i64
42}
43
44#[derive(Model)]
45struct Author {
46 #[column(primary_key, auto)]
47 id: i64,
48 name: String
49}
50
51#[tokio::main]
52async fn main() -> Result<(), Box<dyn std::error::Error>> {
53 // First create a connection. This can be only done once.
54 Connection::build("postgres://postgres:postgres@localhost:5432").connect()?;
55
56 // Then, create tables for your models.
57 // Use `try_create_table!` if you want to fail if a
58 // table with the same name already exists.
59 //
60 // `force_create_table` drops the old table,
61 // which is useful for development.
62 //
63 // If your tables already exist, skip this part.
64 force_create_table!(Author, Book).await?;
65
66 // Next, insert some data.
67 // This works by passing values for all
68 // fields which aren't autogenerated.
69 Author::insert("Stephen King").await?;
70 Author::insert("Martin Luther King").await?;
71 Author::insert("Karl Marx").await?;
72 Book::insert("Foo - Part I", 1).await?;
73 Book::insert("Foo - Part II", 2).await?;
74 Book::insert("Foo - Part III", 3).await?;
75
76 // Let's start with a simple query for all books:
77 let books = Book::select().await?; // Vec<Book>
78 assert_eq!(books.len(), 3);
79
80 // You can also search for a specific book.
81 // Adding a `WHERE` clause is as simple as
82 // calling a method on the respective field:
83 let book = Book::select_one()
84 .where_(Book::title.eq(&"Foo - Part I".to_string()))
85 .await?; // Option<Book>
86 assert!(book.is_some());
87
88 // Or update exsisting records:
89 let books_updated = Book::update()
90 .set(Book::title, &"Foo - Part III".to_string())
91 .where_(Book::title.eq(&"Foo - Part II".to_string()))
92 .await?; // u64
93 assert_eq!(books_updated, 1);
94
95 // Or delete a book, you don't like:
96 let books_deleted = Book::delete()
97 .where_(Book::title.eq(&"Foo - Part III".to_string()))
98 .await?; // u64
99 assert_eq!(books_deleted, 2);
100
101 Ok(())
102}
103```
104
105If you want to see more code examples, have a look at the [tests directory](https://github.com/Einliterflasche/pg-worm/tree/main/pg-worm/tests).
106
107## Query Builders
108As you can see above, `pg-worm` allows you to build queries by chaining methods on so called 'builders'.
109For each query type `pg-worm` provides a respective builder (except for `INSERT` which is handled differently).
110
111These builders expose a set of methods for building queries. Here's a list of them:
112
113Method | Description | Availability
114-------|-------------|-------------
115`where_` | Attach a `WHERE` clause to the query. | All builders (`Select`, `Update`, `Delete`)
116`where_raw` | Same as `where_` but you can pass raw SQL. | All builders (`Select`, `Update`, `Delete`)
117`set` | `SET` a column's value. Note: this method has to be called at least once before you can execute the query. | `Update`
118`limit`, `offset` | Attach a [`LIMIT` or `OFFSET`](https://www.postgresql.org/docs/current/queries-limit.html) to the query. | `Select`
119
120## Filtering using `WHERE`
121`.where_()` can be used to easily include `WHERE` clauses in your queries.
122
123This is done by passing a `Where` object which can be constructed by calling methods on the respective column.
124`pg-worm` automatically constructs a constant for each field
125of your `Model`.
126
127A practical example would look like this:
128
129```ignore
130let where_: Where<'_> = MyModel::my_field.eq(&5);
131```
132
133### Available methods
134
135Currently, the following methods are implemented:
136
137Function | Description | Availability
138---------|-------------|-------------
139`eq` | Checks for equality. | Any type
140`gt`, `gte`, `lt`, `lte` | Check whether this column's value is greater than, etc than some other value. | Any type which implements [`PartialOrd`](https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html). Note: it's not guaranteed that Postgres supports these operator for a type just because it's `PartialOrd`. Be sure to check the Postgres documentation for your type beforehand.
141`null`, `not_null` | Checks whether a column is `NULL`. | Any `Option<T>`. All other types are not `NULL`able and thus guaranteed not to be `NULL`.
142`contains`, `contains_not`, `contains_all`, `conatains_none`, `contains_any` | Array operations. Check whether this column's array contains a value, a value _not_, or any/all/none values of another array. | Any `Vec<T>`.
143
144### Boolean logic
145
146You can also chain/modify these filters with standard boolean logic:
147
148```ignore
149Book::select()
150 .where_(!Book::id.eq(&1) & Book::id.gt(&3))
151 .await?;
152```
153
154Operator/Method | Description
155----------------|------------
156`!`, `.not()` | Negate a filter using a locigal `NOT`
157`&`, `.and()` | Combine two filters using a logical `AND`
158`\|\|`, `.or()` | Combine two filters using a logical `OR`
159
160
161### Executing a query
162
163After having finished building your query, you can simply call `.await`.
164This will turn the builder into a `Query` object which is then executed asynchronously.
165
166Executing a query will always result in a `Result`.
167
168## Raw queries
169
170Though these features are nice, they are not sufficient for all applications. This is why you can easily execute custom queries and still take advantage of automatic parsing, etc:
171
172```ignore
173// NOTE: You have to pass the exact type that PostgreSQL is
174// expecting. Doing otherwise will result in a runtime error.
175let king_books = Book::query(r#"
176 SELECT * FROM book
177 JOIN author ON author.id = book.author_id
178 WHERE POSITION(? in author.name) > 0
179 "#,
180 vec![&"King".to_string()]
181).await?;
182assert_eq!(king_books.len(), 2);
183```
184
185Alse see `.where_raw` on query builders by which you can pass a raw condition without needing to write the whole query yourself.
186
187## Transactions
188
189`pg-worm` also supports transactions. You can easily execute any query inside a `Transaction` and only commit when you are satisfied.
190
191`Transaction`s are automatically rolled-back when dropped, unless they have been committed beforehand.
192
193Here's an example:
194
195```ignore
196use pg_worm::prelude::*;
197
198#[derive(Model)]
199struct Foo {
200 bar: i64
201}
202
203async fn foo() -> Result<(), Box<dyn std::error::Error>> {
204 // Easily create a new transaction
205 let transaction = Transaction::begin().await?;
206
207 // Execute any query inside the transaction
208 let all_foo = transaction.execute(
209 Foo::select()
210 ).await?;
211
212 // Commit the transaction when done.
213 // If not committed, transaction are rolled back
214 // when dropped.
215 transaction.commit().await?;
216}
217```
218
219## Supported types
220The following is a list of supported (Rust) types and which PostgreSQL type they are mapped to.
221
222Rust type | PostgreSQL type
223-------------|---------------------
224`bool` | `BOOL`
225`i16` | `INT2`
226`i32` | `INT4`
227`i64` | `INT8`
228`f32` | `FLOAT4`
229`f64` | `FLOAT8`
230`String` | `TEXT`
231`Option<T>`* | `T` (but the column becomes `NULLABLE`)
232`Vec<T>`* | `T[]`
233
234_*`T` must be another supported type. Nesting and mixing `Option`/`Vec` is currently not supported._
235
236### JSON, timestamps and more
237are supported, too. To use them activate the respective feature, like so:
238
239```ignore
240# Cargo.toml
241[dependencies]
242pg-worm = { version = "latest-version", features = ["foo"] }
243```
244
245Here is a list of the supported features/types with their respective PostgreSQL type:
246
247 * `"serde-json"` for [`serde_json`](https://crates.io/crates/serde_json) `v1.0`
248 Rust type | PostgreSQL type
249 ----------|----------------
250 `Value` | `JSONB`
251 * `"time"` for [`time`](https://crates.io/crates/time/0.3.0) `v3.0`
252 Rust type | PostgreSQL type
253 --------------------|----------------
254 `Date` | `DATE`
255 `Time` | `TIME`
256 `PrimitiveDateTime` | `TIMESTAMP`
257 `OffsetDateTime` | `TIMESTAMP WITH TIME ZONE`
258
259 * `"uuid"` for [`uuid`](https://crates.io/crates/uuid) `v1.0`
260 Rust type | PostgreSQL type
261 ----------|----------------
262 `Uuid` | `UUID`
263
264## `derive` options
265
266You can configure some options for you `Model`.
267This is done by using one of the two attributes `pg-worm` exposes.
268
269### The `#[table]` attribute
270
271The `#[table]` attribute can be used to pass configurations to a `Model` which affect the respective table itself.
272
273```rust
274use pg_worm::prelude::*;
275
276#[derive(Model)]
277#[table(table_name = "book_list")]
278struct Book {
279 id: i64
280}
281```
282
283Option | Meaning | Usage | Default
284-------|---------|-------|--------
285`table_name` | Set the table's name | `table_name = "new_table_name"` | The `struct`'s name converted to snake case using [this crate](https://crates.io/crates/convert_case).
286
287### The `#[column]` attribute
288
289The `#[column]` attribute can be used to pass configurations to a `Model`'s field which affect the respective column.
290
291```rust
292use pg_worm::prelude::*;
293
294#[derive(Model)]
295struct Book {
296 #[column(primary_key, auto)]
297 id: i64
298}
299```
300
301Option | Meaning | Usage | Default
302-------|---------|-------|--------
303`column_name` | Set this column's name. | `#[column(column_name = "new_column_name")]` | The fields's name converted to snake case using [this crate](https://crates.io/crates/convert_case).
304`primary_key` | Make this column the `PRIMARY KEY`. Only use this once per `Model`. If you want this column to be auto generated use `auto` as well. | `#[column(primary_key)]` | `false`
305`auto` | Make this column auto generated. Works only for `i16`, `i32` and `i64`, as well as `Uuid` *if* the `"uuid"` feature has been enabled *and* you use PostgreSQL version 13 or later. | `#[column(auto)]` | `false`
306`unique` | Make this column `UNIQUE`. | `#[column(unique)]` | `false`
307
308## MSRV
309The minimum supported rust version is `1.70` as this crate uses the recently introduced `OnceLock` from the standard library.
310
311## License
312This project is dual-licensed under the MIT and Apache 2.0 licenses.
313
314*/
315
316#![deny(missing_docs)]
317
318// This allows importing this crate's contents from pg-worm-derive.
319extern crate self as pg_worm;
320
321pub mod config;
322pub mod query;
323
324use std::{ops::Deref, sync::OnceLock};
325
326use deadpool_postgres::{Client as DpClient, GenericClient, Pool, Transaction as DpTransaction};
327use pg::types::ToSql;
328use pg::Row;
329use query::{Column, Delete, Query, Select, TypedColumn, Update};
330use thiserror::Error;
331
332#[doc(hidden)]
333pub use async_trait::async_trait;
334#[doc(hidden)]
335pub use futures_util;
336#[doc(hidden)]
337pub use tokio_postgres as pg;
338
339pub use pg_worm_derive::Model;
340
341/// This module contains all necessary imports to get you started
342/// easily.
343pub mod prelude {
344 pub use crate::{force_create_table, try_create_table, FromRow, Model};
345
346 pub use crate::config::Connection;
347
348 pub use crate::query::{
349 Column, Executable, NoneSet, Query, Select, SomeSet, ToQuery, Transaction, TypedColumn,
350 };
351 pub use std::ops::Deref;
352 pub use std::str::FromStr;
353}
354
355/// An enum representing the errors which are emitted by this crate.
356#[non_exhaustive]
357#[derive(Error, Debug)]
358pub enum Error {
359 /// Something went wrong while connection to the database.
360 #[error("not connected to database")]
361 NotConnected,
362 /// There already is a connection to the database.
363 #[error("already connected to database")]
364 AlreadyConnected,
365 /// No connection has yet been established.
366 #[error("couldn't connect to database")]
367 ConnectionError(#[from] deadpool_postgres::CreatePoolError),
368 /// No connection object could be created.
369 #[error("couldn't build connection/config")]
370 ConnectionBuildError(#[from] deadpool_postgres::BuildError),
371 /// Emitted when an invalid config string is passed to `Connection::to`.
372 #[error("invalid config")]
373 ConfigError(#[from] deadpool_postgres::ConfigError),
374 /// Emitted when no connection could be fetched from the pool.
375 #[error("couldn't fetch connection from pool")]
376 PoolError(#[from] deadpool_postgres::PoolError),
377 /// Errors emitted by the Postgres server.
378 ///
379 /// Most likely an invalid query.
380 #[error("postgres returned an error")]
381 PostgresError(#[from] tokio_postgres::Error),
382}
383
384/// A trait signaling that a struct may be parsed from
385/// a Postgres Row.
386///
387/// This being a new trait allows the exposure of a
388/// derive macro for it.
389pub trait FromRow: TryFrom<Row, Error = Error> {}
390
391/// This is the trait which you should derive for your model structs.
392///
393/// It provides the ORM functionality.
394///
395#[async_trait]
396pub trait Model<T>: FromRow {
397 /// This is a library function needed to derive the `Model`trait.
398 ///
399 /// *_DO NOT USE_*
400 #[doc(hidden)]
401 #[must_use]
402 fn _table_creation_sql() -> &'static str;
403
404 /// Returns a slice of all columns this model's table has.
405 fn columns() -> &'static [&'static dyn Deref<Target = Column>];
406
407 /// Returns the name of this model's table's name.
408 fn table_name() -> &'static str;
409
410 /// Start building a `SELECT` query which will be parsed to this model.
411 fn select<'a>() -> Select<'a, Vec<T>>;
412
413 /// Start building a `SELECT` query which returns either
414 /// one entity or `None`.
415 fn select_one<'a>() -> Select<'a, Option<T>>;
416
417 /// Start building an `UPDATE` query.
418 ///
419 /// Returns the number of rows affected.
420 fn update<'a>() -> Update<'a>;
421
422 /// Start building a `DELETE` query.
423 ///
424 /// Returns the number or rows affected.
425 fn delete<'a>() -> Delete<'a>;
426
427 /// Build a raw query by passing in a statement along with
428 /// arguments.
429 ///
430 /// You can reference the params by using `?` as a placeholder.
431 fn query(_: impl Into<String>, _: Vec<&(dyn ToSql + Sync)>) -> Query<'_, Vec<T>>;
432}
433
434static POOL: OnceLock<Pool> = OnceLock::new();
435
436/// Try to fetch a client from the connection pool.
437#[doc(hidden)]
438pub async fn fetch_client() -> Result<DpClient, Error> {
439 POOL.get()
440 .ok_or(Error::NotConnected)?
441 .get()
442 .await
443 .map_err(Error::from)
444}
445
446/// Hidden function so set the pool from the `config` module.
447#[doc(hidden)]
448pub fn set_pool(pool: Pool) -> Result<(), Error> {
449 POOL.set(pool).map_err(|_| Error::AlreadyConnected)
450}
451
452/// Create a table for your model.
453///
454/// Use the [`try_create_table!`] macro for a more convenient api.
455///
456/// # Usage
457/// ```ignore
458/// #[derive(Model)]
459/// struct Foo {
460/// #[column(primary_key)]
461/// id: i64
462/// }
463///
464/// #[tokio::main]
465/// async fn main() -> Result<(), pg_worm::Error> {
466/// // ---- snip connection setup ----
467/// pg_worm::try_create_table::<M>().await?;
468/// }
469/// ```
470#[doc(hidden)]
471pub async fn try_create_table<M: Model<M>>() -> Result<(), Error>
472where
473 Error: From<<M as TryFrom<Row>>::Error>,
474{
475 let client = fetch_client().await?;
476 client.batch_execute(M::_table_creation_sql()).await?;
477
478 Ok(())
479}
480
481/// Same as [`try_create_table`] but if a table with the same name
482/// already exists, it is dropped instead of returning an error.
483#[doc(hidden)]
484pub async fn force_create_table<M: Model<M>>() -> Result<(), Error>
485where
486 Error: From<<M as TryFrom<Row>>::Error>,
487{
488 let client = fetch_client().await?;
489 let query = format!(
490 "DROP TABLE IF EXISTS {} CASCADE; ",
491 M::columns()[0].table_name()
492 ) + M::_table_creation_sql();
493
494 client.batch_execute(&query).await?;
495
496 Ok(())
497}
498
499/// Creates a table for the specified [`Model`].
500///
501/// This is just a more convenient api
502/// for the [`try_create_table()`] function.
503///
504/// Returns an error if:
505/// - a table with the same name already exists,
506/// - the client is not connected,
507/// - the creation of the table fails.
508///
509/// # Usage
510///
511/// ```ignore
512/// use pg_worm::prelude::*;
513///
514/// #[derive(Model)]
515/// struct Foo {
516/// id: i64
517/// }
518///
519/// #[derive(Model)]
520/// struct Bar {
521/// baz: String
522/// }
523///
524/// #[tokio::main]
525/// async fn main() -> Result<(), pg_worm::Error> {
526/// // ---- snip connection setup ----
527/// try_create_table!(Foo, Bar)?;
528/// }
529/// ```
530#[macro_export]
531macro_rules! try_create_table {
532 ($($x:ty),+) => {
533 $crate::futures::future::try_join_all(
534 vec![
535 $(
536 $crate::futures::future::FutureExt::boxed(
537 $crate::try_create_table::<$x>()
538 )
539 ),*
540 ]
541 )
542 };
543}
544
545/// Like [`try_create_table!`] but if a table with the same name already
546/// exists, it is dropped instead of returning an error.
547///
548/// # Example
549/// ```ignore
550/// force_create_table(MyModel, AnotherModel).await?;
551/// ```
552#[macro_export]
553macro_rules! force_create_table {
554 ($($x:ty),+) => {
555 $crate::futures_util::future::try_join_all(
556 vec![
557 $(
558 $crate::futures_util::future::FutureExt::boxed(
559 $crate::force_create_table::<$x>()
560 )
561 ),*
562 ]
563 )
564 };
565}