diff --git a/pg-worm-derive/Cargo.toml b/pg-worm-derive/Cargo.toml index bd91470..edf4c6e 100644 --- a/pg-worm-derive/Cargo.toml +++ b/pg-worm-derive/Cargo.toml @@ -16,6 +16,6 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.56" quote = "1.0.27" -syn = { version = "2.0.15", features = ["full"] } +syn = { version = "2.0.15", features = ["derive"] } darling = "0.20" postgres-types = "0.2" diff --git a/pg-worm-derive/src/parse.rs b/pg-worm-derive/src/parse.rs index 92e4069..393bf75 100644 --- a/pg-worm-derive/src/parse.rs +++ b/pg-worm-derive/src/parse.rs @@ -1,8 +1,8 @@ -use darling::{ast::Data, FromDeriveInput, FromField}; +use darling::{ast::Data, Error, FromDeriveInput, FromField}; use postgres_types::Type; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use syn::{GenericArgument, Ident, PathArguments}; +use syn::{Ident, PathArguments}; #[derive(FromDeriveInput)] #[darling(attributes(table), supports(struct_named))] @@ -18,7 +18,6 @@ pub struct ModelInput { pub struct ModelField { ident: Option, ty: syn::Type, - dtype: Option, column_name: Option, #[darling(default)] auto: bool, @@ -64,15 +63,15 @@ impl ModelInput { /// Generate the SQL statement needed to create /// the table corresponding to the input. - fn table_creation_sql(&self) -> String { - format!( + fn table_creation_sql(&self) -> Result { + Ok(format!( "CREATE TABLE {} ({})", self.table_name(), self.all_fields() .map(|f| f.column_creation_sql()) - .collect::>() + .collect::, Error>>()? .join(", ") - ) + )) } /// Generate all code needed. @@ -102,7 +101,10 @@ impl ModelInput { fn impl_model(&self) -> TokenStream { let ident = self.ident(); let table_name = self.table_name(); - let creation_sql = self.table_creation_sql(); + let creation_sql = match self.table_creation_sql() { + Ok(res) => quote!(#res), + Err(err) => err.write_errors(), + }; let select = self.impl_select(); let delete = self.impl_delete(); @@ -314,6 +316,12 @@ impl ModelInput { } } +macro_rules! spanned_error { + ($msg:expr, $err:expr) => { + return Err(darling::Error::custom($msg).with_span($err)) + }; +} + impl ModelField { /// Initialization function called before each /// field is stored. @@ -322,10 +330,12 @@ impl ModelField { // Extract relevant type from the path let syn::Type::Path(path) = ty else { - panic!("field type must be valid path"); + spanned_error!("unsupported type", &ty) }; let path = &path.path; - let last_seg = path.segments.last().expect("must provide type"); + let Some(last_seg) = path.segments.last() else { + spanned_error!("invalid path (needs at least one segment)", &ty) + }; match last_seg.ident.to_string().as_str() { // If it's an Option, set the field nullable @@ -354,102 +364,61 @@ impl ModelField { self.ident().to_string().to_lowercase() } - /// Get the corresponding column's PostgreSQL datatype. - fn pg_datatype(&self) -> Type { - fn from_str(ty: &str) -> Type { - match ty { - "bool" | "boolean" => Type::BOOL, - "text" => Type::TEXT, - "int" | "integer" | "int4" => Type::INT4, - "bigint" | "int8" => Type::INT8, - "smallint" | "int2" => Type::INT2, - "real" => Type::FLOAT4, - "double precision" => Type::FLOAT8, - "bigserial" => Type::INT8, - _ => panic!("couldn't find postgres type `{}`", ty), - } - } + /// Get the corresponding postgres type + fn try_pg_datatype(&self) -> Result { + let ty = self.ty.clone(); - fn from_type(ty: &Ident) -> Type { - match ty.to_string().as_str() { - "String" => Type::TEXT, - "i32" => Type::INT4, - "i64" => Type::INT8, - "f32" => Type::FLOAT4, - "f64" => Type::FLOAT8, - "bool" => Type::BOOL, - _ => panic!("cannot map rust type to postgres type: {ty}"), - } - } - - if let Some(dtype) = &self.dtype { - return from_str(dtype.as_str()); - } - - let syn::Type::Path(type_path) = &self.ty else { - panic!("field type must be path; no reference, impl, etc. allowed") + let syn::Type::Path(path) = &self.ty else { + spanned_error!("pg-worm: unsupported type, must be a TypePath", &ty) }; - let segment = type_path - .path - .segments - .last() - .expect("field type must have a last segment"); - let args = &segment.arguments; - - if segment.ident.to_string().as_str() == "Option" { - // Extract `T` from `Option` - let PathArguments::AngleBracketed(args) = args else { - panic!("field of type option needs angle bracketed argument") - }; - let GenericArgument::Type(arg) = args.args.first().expect("Option needs to have generic argument") else { - panic!("generic argument for Option must be concrete type") - }; - let syn::Type::Path(type_path) = arg else { - panic!("generic arg for Option must be path") - }; + let Some(segment) = path.path.segments.last() else { + spanned_error!("pg-worm: unsupported type path, must have at least one segment", &ty) + }; - let ident = &type_path - .path - .segments - .first() - .expect("generic arg for Option must have segment") - .ident; + let mut id = &segment.ident; - return from_type(ident); - } + if self.array || self.nullable { + let PathArguments::AngleBracketed(args) = &segment.arguments else { + spanned_error!("pg-worm: unsupported type, Option/Vec need generic argument", &ty) + }; - if segment.ident.to_string().as_str() == "Vec" { - // Extract `T` from `Option` - let PathArguments::AngleBracketed(args) = args else { - panic!("field of type Vec needs angle bracketed argument") + let Some(arg) = args.args.first() else { + spanned_error!("pg-worm: unsupported type, Option/Vec need generic argument", &ty) }; - let GenericArgument::Type(arg) = args.args.first().expect("Vec needs to have generic argument") else { - panic!("generic argument for Vec must be concrete type") + + let syn::GenericArgument::Type(arg_type) = arg else { + spanned_error!("pg-worm: unsupported Option/Vec generic argument, must be valid type", &ty) }; - let syn::Type::Path(type_path) = arg else { - panic!("generic arg for Vec must be path") + + let syn::Type::Path(path) = &arg_type else { + spanned_error!("pg-worm: unsupported type, must be a TypePath", &ty) }; - let ident = &type_path - .path - .segments - .first() - .expect("generic arg for Vec must have segment") - .ident; + let Some(segment) = path.path.segments.last() else { + spanned_error!("pg-worm: unsupported type path, must have at least one segment", &ty) + }; - return from_type(ident); + id = &segment.ident; } - from_type(&segment.ident) + Ok(match id.to_string().as_ref() { + "String" => Type::TEXT, + "i32" => Type::INT4, + "i64" => Type::INT8, + "f32" => Type::FLOAT4, + "f64" => Type::FLOAT8, + "bool" => Type::BOOL, + _ => spanned_error!("pg-worm: unsupported type, check docs", &ty), + }) } /// Get the SQL representing the column needed /// for creating a table. - fn column_creation_sql(&self) -> String { + fn column_creation_sql(&self) -> Result { // The list of "args" for the sql statement. // Includes at least the column name and datatype. - let mut args = vec![self.column_name(), self.pg_datatype().to_string()]; + let mut args = vec![self.column_name(), self.try_pg_datatype()?.to_string()]; // This macro allows adding an arg to the list // under a given condition. @@ -469,7 +438,7 @@ impl ModelField { arg!(!(self.primary_key || self.nullable), "NOT NULL"); // Join the args, seperated by a space and return them - args.join(" ") + Ok(args.join(" ")) } /// The datatype which should be provided when diff --git a/pg-worm/Cargo.toml b/pg-worm/Cargo.toml index 1490188..07e9dfe 100644 --- a/pg-worm/Cargo.toml +++ b/pg-worm/Cargo.toml @@ -15,6 +15,7 @@ thiserror = "1.0" deadpool-postgres = "0.10" tokio-postgres = "0.7" async-trait = "0.1" +futures = "0.3" pg-worm-derive = { version = "0.5", path = "../pg-worm-derive" } diff --git a/pg-worm/README.md b/pg-worm/README.md index 65124e5..573bb9c 100644 --- a/pg-worm/README.md +++ b/pg-worm/README.md @@ -1,26 +1,26 @@ -![GitHub Actions Testing](https://github.com/Einliterflasche/pg-worm/actions/workflows/rust.yml/badge.svg) - # `pg-worm` +[![Latest Version](https://img.shields.io/crates/v/pg-worm.svg)](https://crates.io/crates/pg-worm) +![GitHub Actions Testing](https://github.com/Einliterflasche/pg-worm/actions/workflows/rust.yml/badge.svg) +[![docs](https://docs.rs/pg-worm/badge.svg)](https://docs.rs/pg-worm) +[![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + ### *P*ost*g*reSQL's *W*orst *ORM* `pg-worm` is a straightforward, fully typed, async ORM and Query Builder for PostgreSQL. Well, at least that's the goal. -## Motivation +## Why though? -Existing ORMs are not `async`, require you to write migrations or use a cli. -`pg_worm`'s explicit goal is to be easy and to require no setup beyond defining your types. +- Existing ORMs are not **`async`**, require you to write migrations or use a cli. +`pg-worm's explicit goal is to be **easy** and to require **no setup** beyond defining your types. -`pg_worm` also has no DSL which you need to learn. -Everything you can do maps intuitively and directly to SQL expressions. +- `pg-worm also features **built-in pooling** and a **concise syntax**. -And, last but not least, `pg_worm` **does not get in your way**. Instead of relying on weird DSL tricks to achieve complex queries, you can simply write your own SQL and still profit off automatic parsing, etc. - -But that's enough ranting, go ahead and try it! +- `pg-worm **doesn't get in your way** - easily include raw queries while still profiting off the other features. ## Usage 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/). -Fortunately, using `pg_worm` is very easy. +Fortunately, using `pg-worm is very easy. Simply derive the `Model` trait for your type, connect to your database and you are ready to go! @@ -28,6 +28,7 @@ and you are ready to go! Here's a quick example: ```rust +// Import the prelude to get started quickly use pg_worm::prelude::*; #[derive(Model)] @@ -53,14 +54,14 @@ async fn main() -> Result<(), Box> { Connection::to("postgres://postgres:postgres@localhost:5432").await?; // Then, create tables for your models. - // Use `register!` if you want to fail if a + // Use `try_create_table!` if you want to fail if a // table with the same name already exists. // - // `force_register` drops the old table, + // `force_create_table` drops the old table, // which is useful for development. // // If your tables already exist, skip this part. - force_register!(Author, Book)?; + force_create_table!(Author, Book).await?; // Next, insert some data. // This works by passing values for all @@ -104,22 +105,23 @@ async fn main() -> Result<(), Box> { If 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). ## Query Builders -As you can see in the above example, `pg_worm` allows you to build queries by chaining methods on so called 'builders'. -For each query type `pg_worm` provides a respective builder (except for `INSERT` which is handled differently). +As you can see above, `pg-worm allows you to build queries by chaining methods on so called 'builders'. +For each query type `pg-worm provides a respective builder (except for `INSERT` which is handled differently). These builders expose a set of methods for building queries. Here's a list of them: Method | Description | Availability -------|-------------|------------- `where_` | Attach a `WHERE` clause to the query. | All builders (`Select`, `Update`, `Delete`) +`where_raw` | Same as `where_` but you can pass raw SQL. | All builders (`Select`, `Update`, `Delete`) `set` | `SET` a column's value. Note: this method has to be called at least once before you can execute the query. | `Update` `limit`, `offset` | Attach a [`LIMIT` or `OFFSET`](https://www.postgresql.org/docs/current/queries-limit.html) to the query. | `Select` ## Filtering using `WHERE` -`where_()` can be used to easily include `WHERE` clauses in your queries. +`.where_()` can be used to easily include `WHERE` clauses in your queries. This is done by passing a `Where` object which can be constructed by calling methods on the respective column. -`pg_worm` automatically constructs a constant for each field +`pg-worm automatically constructs a constant for each field of your `Model`. A practical example would look like this: @@ -127,6 +129,7 @@ A practical example would look like this: ```rust let where_: Where<'_> = MyModel::my_field.eq(&5); ``` + ### Available methods Currently, the following methods are implemented: @@ -134,7 +137,7 @@ Currently, the following methods are implemented: Function | Description | Availability ---------|-------------|------------- `eq` | Checks for equality. | Any type -`gt`, `gte`, `lt`, `lte` | Check whether this column's value is greater than, etc than some other value. | Any type which implement [`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 documentation beforehand. +`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. `null`, `not_null` | Checks whether a column is `NULL`. | Any `Option`. All other types are not `NULL`able and thus guaranteed not to be `NULL`. `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`. @@ -142,11 +145,18 @@ Function | Description | Availability You can also chain/modify these filters with standard boolean logic: +```rust +Book::select() + .where_(!Book::id.eq(&1) & Book::id.gt(&3)) + .await?; +``` + Operator/Method | Description ----------------|------------ -`!`, `not` | Negate a filter using a locigal `NOT` -`&`, `and` | Combine two filters using a logical `AND`. -`\|`, `or` | Combine two filters using a logical `OR` +`!`, `.not()` | Negate a filter using a locigal `NOT` +`&`, `.and()` | Combine two filters using a logical `AND` +`\|\|`, `.or()` | Combine two filters using a logical `OR` + ### Executing a query @@ -157,10 +167,10 @@ Executing a query will always result in a `Result`. ## Raw queries -Though these features are nice, they are not sufficient for most applications. This is why you can easily execute custom queries and still take advantage of automatic parsing, etc: +Though 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: ```rust -// NOTE: You have to pass the exact type that Postgres is +// NOTE: You have to pass the exact type that PostgreSQL is // expecting. Doing otherwise will result in a runtime error. let king_books = Book::query(r#" SELECT * FROM book @@ -172,8 +182,57 @@ let king_books = Book::query(r#" assert_eq!(king_books.len(), 2); ``` +Alse see `.where_raw` on query builders by which you can pass a raw condition without needing to write the whole query yourself. + +## Transactions + +`pg-worm also supports transactions. You can easily execute any query inside a `Transaction` and only commit when you are satisfied. + +`Transaction`s are automatically rolled-back when dropped, unless they have been committed beforehand. + +Here's an example: + +```rust +use pg_worm::prelude::*; + +#[derive(Model)] +struct Foo { + bar: i64 +} + +async fn foo() -> Result<(), Box> { + // Easily create a new transaction + let transaction = Transaction::begin().await?; + + // Execute any query inside the transaction + let all_foo = transaction.execute( + Foo::select() + ).await?; + + // Commit the transaction when done. + // If not committed, transaction are rolled back + // when dropped. + transaction.commit().await?; +} +``` + +## Supported types +The following is a list of all supported (Rust) types and which PostgreSQL type they are mapped to. +Rust type | PostgreSQL type +----------|--------------------- +`bool` | `bool` +`i32` | `int4` +`i64` | `int8` +`f32` | `float4` +`f64` | `float8` +`String` | `TEXT` +`Option`* | `T` (but the column becomes `nullable`) +`Vec`* | `T[]` + +_*`T` must be another supported type. Nesting and mixing `Option`/`Vec` is currently not supported._ + ## MSRV -The minimum supported version of rust is `1.70` as this crate uses the recently introduced `OnceLock` from the standard library. +The minimum supported rust version is `1.70` as this crate uses the recently introduced `OnceLock` from the standard library. ## License This project is dual-licensed under the MIT and Apache 2.0 licenses. diff --git a/pg-worm/src/config.rs b/pg-worm/src/config.rs index ecd4539..0b63fb1 100644 --- a/pg-worm/src/config.rs +++ b/pg-worm/src/config.rs @@ -1,43 +1,129 @@ -//! +//! This module contains the code for configuring the connection pool. use std::str::FromStr; -use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod}; +use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod, Runtime}; use tokio_postgres::{ tls::{MakeTlsConnect, TlsConnect}, Config, NoTls, Socket, }; -use crate::Error; +/// An empty struct that only provides the `build` method. +pub struct Connection; /// -pub struct Connection; +pub struct ConnectionBuilder +where + Tls: MakeTlsConnect + Clone + Send + Sync + 'static, + Tls::Stream: Sync + Send, + Tls::TlsConnect: Sync + Send, + >::Future: Send, +{ + url: String, + tls: Tls, + recycling_method: RecyclingMethod, + max_pool_size: Option, + runtime: Option, +} -impl Connection { - /// Connect `pg_worm` to postgres using the specified connection string - /// and tls. - pub async fn to_tls(connection_string: impl Into, tls: T) -> Result<(), Error> +impl ConnectionBuilder +where + Tls: MakeTlsConnect + Clone + Send + Sync + 'static, + Tls::Stream: Sync + Send, + Tls::TlsConnect: Sync + Send, + >::Future: Send, +{ + fn to(url: impl Into) -> ConnectionBuilder { + ConnectionBuilder { + url: url.into(), + tls: NoTls, + recycling_method: RecyclingMethod::Fast, + max_pool_size: None, + runtime: None, + } + } + + /// Set the Tls method. + /// + /// Use either [`postgres-openssl`](https://crates.io/crates/postgres-openssl) + /// or [`postgres-nativ-tls`](https://crates.io/crates/postgres-native-tls) + /// and their respective documentation. + /// This function accepts the same types as `tokio-postgres`. + pub fn tls(self, tls: NewTls) -> ConnectionBuilder where - T: MakeTlsConnect + Clone + Send + Sync + 'static, - T::Stream: Sync + Send, - T::TlsConnect: Sync + Send, - >::Future: Send, + NewTls: MakeTlsConnect + Clone + Send + Sync + 'static, + NewTls::Stream: Sync + Send, + NewTls::TlsConnect: Sync + Send, + >::Future: Send, { - let config = Config::from_str(connection_string.into().as_str())?; + ConnectionBuilder { + tls, + url: self.url, + recycling_method: self.recycling_method, + runtime: self.runtime, + max_pool_size: self.max_pool_size, + } + } + + /// Set the maximum size of the connection pool, + /// i.e. the maximum amount of concurrent connections to the database server. + /// + /// The default is `num_cpus * 4`, ignoring hyperthreading, etc. + pub fn max_pool_size(mut self, n: usize) -> Self { + self.max_pool_size = Some(n); + + self + } + + /// Finish the setup and build the pool. + /// + /// Fails if + /// - the url couldn't be parsed, or + /// - some other configuration error has been made. + pub fn connect(self) -> Result<(), crate::Error> { + let config = Config::from_str(&self.url)?; let manager_config = ManagerConfig { - recycling_method: RecyclingMethod::Fast, + recycling_method: self.recycling_method, }; - let manager = Manager::from_config(config, tls, manager_config); + let manager = Manager::from_config(config, self.tls, manager_config); + let mut builder = Pool::builder(manager).runtime(Runtime::Tokio1); - let pool = Pool::builder(manager).max_size(4).build()?; + if let Some(n) = self.max_pool_size { + builder = builder.max_size(n); + } - crate::set_pool(pool) + let pool = builder.build()?; + + crate::set_pool(pool)?; + + Ok(()) } +} - /// Connect to a postgres server without using TLS - /// (only recommended for local databases). - pub async fn to(connection_string: impl Into) -> Result<(), Error> { - Self::to_tls(connection_string, NoTls).await +impl Connection { + /// Start building a new connection (pool). + /// + /// This returns a [`ConnectionBuilder`] which can be configured + /// using the builder pattern. + /// + /// If you are fine with the default configuration + /// (`max_pool_size = num_cpus * 4` and no Tls) or have + /// configured to your needs you can finish the setup + /// by calling `.connect()`. + /// + /// A connection must be created before executing any + /// queries or the like. + /// Doing otherwise will result in a runime error. + /// + /// # Example + /// ```ignore + /// use pg_worm::prelude::*; + /// + /// Connection::build("postgres://postgres").connect()?; + /// ``` + /// + pub fn build(connection_string: impl Into) -> ConnectionBuilder { + ConnectionBuilder::::to(connection_string) } } diff --git a/pg-worm/src/lib.rs b/pg-worm/src/lib.rs index d259452..0f8020c 100644 --- a/pg-worm/src/lib.rs +++ b/pg-worm/src/lib.rs @@ -1,31 +1,43 @@ /*! # `pg-worm` +[![Latest Version](https://img.shields.io/crates/v/pg-worm.svg)](https://crates.io/crates/pg-worm) +![GitHub Actions Testing](https://github.com/Einliterflasche/pg-worm/actions/workflows/rust.yml/badge.svg) +[![docs](https://docs.rs/pg-worm/badge.svg)](https://docs.rs/pg-worm) +[![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ### *P*ost*g*reSQL's *W*orst *ORM* - `pg-worm` is a straightforward, fully typed, async ORM and Query Builder for PostgreSQL. -Well, at least that's the goal. +Well, at least that's the goal. + +## Features/Why `pg-worm? + +- Existing ORMs are not **`async`**, require you to write migrations or use a cli. +`pg-worm's explicit goal is to be **easy** and to require **no setup** beyond defining your types. -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/). +- `pg-worm also features **built-in pooling** and a **concise syntax**. + +- `pg-worm **doesn't get in your way** - easily include raw queries while still profiting off the other features. ## Usage -Fortunately, using `pg_worm` is very easy. +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/). + +Fortunately, using `pg-worm is very easy. -Simply derive the [`Model`] trait for your type, connect to your database +Simply derive the `Model` trait for your type, connect to your database and you are ready to go! -Here's a quick example: +Here's a quick example: ```rust +// Import the prelude to get started quickly use pg_worm::prelude::*; #[derive(Model)] struct Book { - // An auto-generated primary key column + // An auto-generated primary key #[column(primary_key, auto)] id: i64, - title: String, + title: String author_id: i64 } @@ -33,7 +45,8 @@ struct Book { struct Author { #[column(primary_key, auto)] id: i64, - name: String + name: String, + age: i64 } #[tokio::main] @@ -41,15 +54,15 @@ async fn main() -> Result<(), Box> { // First create a connection. This can be only done once. Connection::to("postgres://postgres:postgres@localhost:5432").await?; - // Then, create tables for your models. - // Use `register!` if you want to fail if a + // Then, create tables for your models. + // Use `try_create_table!` if you want to fail if a // table with the same name already exists. // - // `force_register` drops the old table, + // `force_create_table` drops the old table, // which is useful for development. // // If your tables already exist, skip this part. - force_register!(Author, Book)?; + force_create_table!(Author, Book).await?; // Next, insert some data. // This works by passing values for all @@ -93,29 +106,31 @@ async fn main() -> Result<(), Box> { If 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). ## Query Builders -As you can see in the above example, `pg_worm` allows you to build queries by chaining methods on so called 'builders'. -For each query type `pg_worm` provides a respective builder (except for `INSERT` which is handled differently). +As you can see above, `pg-worm allows you to build queries by chaining methods on so called 'builders'. +For each query type `pg-worm provides a respective builder (except for `INSERT` which is handled differently). These builders expose a set of methods for building queries. Here's a list of them: Method | Description | Availability -------|-------------|------------- -`where_` | Attach a `WHERE` clause to the query. | All builders ([`Select`], [`Update`], [`Delete`]) +`where_` | Attach a `WHERE` clause to the query. | All builders (`Select`, `Update`, `Delete`) +`where_raw` | Same as `where_` but you can pass raw SQL. | All builders (`Select`, `Update`, `Delete`) `set` | `SET` a column's value. Note: this method has to be called at least once before you can execute the query. | `Update` `limit`, `offset` | Attach a [`LIMIT` or `OFFSET`](https://www.postgresql.org/docs/current/queries-limit.html) to the query. | `Select` ## Filtering using `WHERE` -`where_()` can be used to easily include `WHERE` clauses in your queries. +`.where_()` can be used to easily include `WHERE` clauses in your queries. -This is done by passing a [`Where`] object which can be constructed by calling methods on the respective column. -`pg_worm` automatically constructs a constant for each field +This is done by passing a `Where` object which can be constructed by calling methods on the respective column. +`pg-worm automatically constructs a constant for each field of your `Model`. -A practical example could look like this: +A practical example would look like this: -```ignore +```rust let where_: Where<'_> = MyModel::my_field.eq(&5); ``` + ### Available methods Currently, the following methods are implemented: @@ -123,7 +138,7 @@ Currently, the following methods are implemented: Function | Description | Availability ---------|-------------|------------- `eq` | Checks for equality. | Any type -`gt`, `gte`, `lt`, `lte` | Check whether this column's value is greater than, etc than some other value. | Any type which implement [`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 documentation beforehand. +`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. `null`, `not_null` | Checks whether a column is `NULL`. | Any `Option`. All other types are not `NULL`able and thus guaranteed not to be `NULL`. `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`. @@ -131,37 +146,94 @@ Function | Description | Availability You can also chain/modify these filters with standard boolean logic: +```rust +Book::select() + .where_(!Book::id.eq(&1) & Book::id.gt(&3)) + .await?; +``` + Operator/Method | Description ----------------|------------ -`!`, `not` | Negate a filter using a locigal `NOT` -`&`, `and` | Combine two filters using a logical `AND`. -`\|`, `or` | Combine two filters using a logical `OR`. +`!`, `.not()` | Negate a filter using a locigal `NOT` +`&`, `.and()` | Combine two filters using a logical `AND` +`\|\|`, `.or()` | Combine two filters using a logical `OR` + ### Executing a query -After having finished building your query, you can simply call `.await`. -This will turn the builder into a [`Query`] object which is then executed asynchronously. +After having finished building your query, you can simply call `.await`. +This will turn the builder into a `Query` object which is then executed asynchronously. -A query will always return a `Result`. +Executing a query will always result in a `Result`. ## Raw queries -Though these features are nice, they are not sufficient for most applications. This is why you can easily execute custom queries and still take advantage of automatic parsing, etc: +Though 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: -```ignore -// NOTE: You have to pass the exact type that Postgres is +```rust +// NOTE: You have to pass the exact type that PostgreSQL is // expecting. Doing otherwise will result in a runtime error. let king_books = Book::query(r#" - SELECT * FROM book + SELECT * FROM book JOIN author ON author.id = book.author_id - WHERE POSITION(? in author.name) > 0 - "#, + WHERE POSITION(? in author.name) > 0 + "#, vec![&"King".to_string()] ).await?; assert_eq!(king_books.len(), 2); ``` + +Alse see `.where_raw` on query builders by which you can pass a raw condition without needing to write the whole query yourself. + +## Transactions + +`pg-worm also supports transactions. You can easily execute any query inside a `Transaction` and only commit when you are satisfied. + +`Transaction`s are automatically rolled-back when dropped, unless they have been committed beforehand. + +Here's an example: + +```rust +use pg_worm::prelude::*; + +#[derive(Model)] +struct Foo { + bar: i64 +} + +async fn foo() -> Result<(), Box> { + // Easily create a new transaction + let transaction = Transaction::begin().await?; + + // Execute any query inside the transaction + let all_foo = transaction.execute( + Foo::select() + ).await?; + + // Commit the transaction when done. + // If not committed, transaction are rolled back + // when dropped. + transaction.commit().await?; +} +``` + +## Supported types +The following is a list of all supported (Rust) types and which PostgreSQL type they are mapped to. +Rust type | PostgreSQL type +----------|--------------------- +`bool` | `bool` +`i32` | `int4` +`i64` | `int8` +`f32` | `float4` +`f64` | `float8` +`String` | `TEXT` +`Option`* | `T` (but the column becomes `nullable`) +`Vec`* | `T[]` + +_*`T` must be another supported type. Nesting and mixing `Option`/`Vec` is currently not supported._ + ## MSRV -The minimum supported version of rust is `1.70` as this crate uses the recently introduced `OnceLock` from the standard library. +The minimum supported rust version is `1.70` as this crate uses the recently introduced `OnceLock` from the standard library. ## License This project is dual-licensed under the MIT and Apache 2.0 licenses. @@ -184,6 +256,8 @@ use query::{Delete, Update}; use crate::query::Select; pub use async_trait::async_trait; +/// This crate's reexport of the `futures` crate. +pub use futures; pub use pg::{NoTls, Row}; pub use pg_worm_derive::Model; /// This crate's reexport of the `tokio_postgres` crate. @@ -195,7 +269,7 @@ use thiserror::Error; /// This module contains all necessary imports to get you started /// easily. pub mod prelude { - pub use crate::{force_register, register, FromRow, Model}; + pub use crate::{force_create_table, try_create_table, FromRow, Model}; pub use crate::config::Connection; @@ -207,30 +281,31 @@ pub mod prelude { } /// An enum representing the errors which are emitted by this crate. +#[non_exhaustive] #[derive(Error, Debug)] pub enum Error { /// Something went wrong while connection to the database. - #[error("couldn't connect to database")] + #[error("not connected to database")] NotConnected, /// There already is a connection to the database. #[error("already connected to database")] AlreadyConnected, /// No connection has yet been established. - #[error("not connected to database")] + #[error("couldn't connect to database")] ConnectionError(#[from] deadpool_postgres::CreatePoolError), /// No connection object could be created. #[error("couldn't build connection/config")] ConnectionBuildError(#[from] deadpool_postgres::BuildError), - /// Emitted + /// Emitted when an invalid config string is passed to `Connection::to`. #[error("invalid config")] ConfigError(#[from] deadpool_postgres::ConfigError), - /// name + /// Emitted when no connection could be fetched from the pool. #[error("couldn't fetch connection from pool")] PoolError(#[from] deadpool_postgres::PoolError), /// Errors emitted by the Postgres server. /// /// Most likely an invalid query. - #[error("error communicating with database")] + #[error("postgres returned an error")] PostgresError(#[from] tokio_postgres::Error), } @@ -304,7 +379,7 @@ pub fn set_pool(pool: Pool) -> Result<(), Error> { /// Create a table for your model. /// -/// Use the [`register!`] macro for a more convenient api. +/// Use the [`try_create_table!`] macro for a more convenient api. /// /// # Usage /// ```ignore @@ -317,10 +392,11 @@ pub fn set_pool(pool: Pool) -> Result<(), Error> { /// #[tokio::main] /// async fn main() -> Result<(), pg_worm::Error> { /// // ---- snip connection setup ---- -/// pg_worm::register_model::().await?; +/// pg_worm::try_create_table::().await?; /// } /// ``` -pub async fn register_model>() -> Result<(), Error> +#[doc(hidden)] +pub async fn try_create_table>() -> Result<(), Error> where Error: From<>::Error>, { @@ -330,9 +406,10 @@ where Ok(()) } -/// Same as [`register_model`] but if a table with the same name +/// Same as [`try_create_table`] but if a table with the same name /// already exists, it is dropped instead of returning an error. -pub async fn force_register_model>() -> Result<(), Error> +#[doc(hidden)] +pub async fn force_create_table>() -> Result<(), Error> where Error: From<>::Error>, { @@ -347,18 +424,15 @@ where Ok(()) } -/// Registers a [`Model`] with the database by creating a -/// corresponding table. +/// Creates a table for the specified [`Model`]. /// -/// This is just a more convenient version api -/// for the [`register_model`] function. -/// -/// This macro, too, requires the `tokio` crate. +/// This is just a more convenient api +/// for the [`try_create_table()`] function. /// /// Returns an error if: /// - a table with the same name already exists, /// - the client is not connected, -/// - the creation of the table fails +/// - the creation of the table fails. /// /// # Usage /// @@ -370,28 +444,50 @@ where /// id: i64 /// } /// +/// #[derive(Model)] +/// struct Bar { +/// baz: String +/// } +/// /// #[tokio::main] /// async fn main() -> Result<(), pg_worm::Error> { /// // ---- snip connection setup ---- -/// register!(Foo)?; +/// try_create_table!(Foo, Bar)?; /// } /// ``` #[macro_export] -macro_rules! register { +macro_rules! try_create_table { ($($x:ty),+) => { - tokio::try_join!( - $($crate::register_model::<$x>()),* + $crate::futures::future::try_join_all( + vec![ + $( + $crate::futures::future::FutureExt::boxed( + $crate::try_create_table::<$x>() + ) + ),* + ] ) }; } -/// Like [`register!`] but if a table with the same name already +/// Like [`try_create_table!`] but if a table with the same name already /// exists, it is dropped instead of returning an error. +/// +/// # Example +/// ```ignore +/// force_create_table(MyModel, AnotherModel).await?; +/// ``` #[macro_export] -macro_rules! force_register { +macro_rules! force_create_table { ($($x:ty),+) => { - tokio::try_join!( - $($crate::force_register_model::<$x>()),* + $crate::futures::future::try_join_all( + vec![ + $( + $crate::futures::future::FutureExt::boxed( + $crate::force_create_table::<$x>() + ) + ),* + ] ) }; } diff --git a/pg-worm/src/query/delete.rs b/pg-worm/src/query/delete.rs index bc2f1ab..07fb61d 100644 --- a/pg-worm/src/query/delete.rs +++ b/pg-worm/src/query/delete.rs @@ -3,6 +3,8 @@ use std::{ pin::Pin, }; +use tokio_postgres::types::ToSql; + use super::{Executable, PushChunk, ToQuery, Where}; /// A struct for building `DELETE` queries. @@ -30,6 +32,33 @@ impl<'a> Delete<'a> { self } + + /// Add a raw `WHERE` clause to your query. + /// + /// You can reference the `params` by using the `?` placeholder in your statement. + /// + /// Note: you need to pass the exact types Postgres is expecting. + /// Failure to do so will result in (sometimes confusing) runtime errors. + /// + /// Otherwise this behaves exactly like `where_`. + /// + /// # Example + /// + /// ```ignore + /// Book::select() + /// .where_(Book::id.neq(&3)) + /// .where_raw("complex_function(book.title, ?, ?)", vec![&true, &"Foobar"]) + /// .await?; + /// ``` + pub fn where_raw( + self, + statement: impl Into, + params: Vec<&'a (dyn ToSql + Sync)>, + ) -> Delete<'a> { + let where_ = Where::new(statement.into(), params); + + self.where_(where_) + } } impl<'a> PushChunk<'a> for Delete<'a> { diff --git a/pg-worm/src/query/mod.rs b/pg-worm/src/query/mod.rs index 647638e..7fa3ea7 100644 --- a/pg-worm/src/query/mod.rs +++ b/pg-worm/src/query/mod.rs @@ -25,13 +25,16 @@ pub use delete::Delete; pub use select::Select; pub use transaction::*; pub use update::{NoneSet, SomeSet, Update}; + /// A trait implemented by everything that goes inside a query. +#[doc(hidden)] pub trait PushChunk<'a> { /// Pushes the containing string and the params to the provided buffer. fn push_to_buffer(&mut self, buffer: &mut Query<'a, T>); } -/// A trait for abstracting over clients/transactions. +/// A trait abstracting over `Client`s and `Transaction`s. +#[doc(hidden)] #[async_trait] pub trait Executor { /// Maps to tokio_postgres::Client::query. @@ -53,7 +56,7 @@ pub trait Executable { self.exec_with(&client).await } - /// + /// Execute the query given any viable `Executor` async fn exec_with( &self, client: impl Executor + Send + Sync, @@ -89,6 +92,7 @@ pub trait ToQuery<'a, T>: PushChunk<'a> { /// /// This is bundes the params with the relevant part of the statement /// and thus makes ordering them much easier. +#[doc(hidden)] pub struct SqlChunk<'a>(pub String, pub Vec<&'a (dyn ToSql + Sync)>); /// A generic implementation of `IntoFuture` for all viable query builders diff --git a/pg-worm/src/query/select.rs b/pg-worm/src/query/select.rs index 5066059..5b05107 100644 --- a/pg-worm/src/query/select.rs +++ b/pg-worm/src/query/select.rs @@ -5,7 +5,7 @@ use std::{ pin::Pin, }; -use tokio_postgres::Row; +use tokio_postgres::{types::ToSql, Row}; use super::{Executable, PushChunk, Query, ToQuery, Where}; use crate::Column; @@ -50,6 +50,33 @@ impl<'a, T> Select<'a, T> { self } + /// Add a raw `WHERE` clause to your query. + /// + /// You can reference the `params` by using the `?` placeholder in your statement. + /// + /// Note: you need to pass the exact types Postgres is expecting. + /// Failure to do so will result in (sometimes confusing) runtime errors. + /// + /// Otherwise this behaves exactly like `where_`. + /// + /// # Example + /// + /// ```ignore + /// Book::select() + /// .where_(Book::id.neq(&3)) + /// .where_raw("complex_function(book.title, ?, ?)", vec![&true, &"Foobar"]) + /// .await?; + /// ``` + pub fn where_raw( + self, + statement: impl Into, + params: Vec<&'a (dyn ToSql + Sync)>, + ) -> Select<'a, T> { + let where_ = Where::new(statement.into(), params); + + self.where_(where_) + } + /// Add a `LIMIT` to your query. pub fn limit(mut self, limit: u64) -> Select<'a, T> { self.limit = Some(limit); diff --git a/pg-worm/src/query/update.rs b/pg-worm/src/query/update.rs index 89f2c65..3fb5a95 100644 --- a/pg-worm/src/query/update.rs +++ b/pg-worm/src/query/update.rs @@ -14,9 +14,11 @@ use super::{push_all_with_sep, Executable, PushChunk, Query, SqlChunk, ToQuery, /// has been set. /// /// `UPDATE` queries in this state cannot be executed. +#[doc(hidden)] pub struct NoneSet; /// State representing that an UDPATE /// has been set. +#[doc(hidden)] pub struct SomeSet; /// A struct for building `UPDATE` queries. @@ -53,6 +55,33 @@ impl<'a, T> Update<'a, T> { self } + /// Add a raw `WHERE` clause to your query. + /// + /// You can reference the `params` by using the `?` placeholder in your statement. + /// + /// Note: you need to pass the exact types Postgres is expecting. + /// Failure to do so will result in (sometimes confusing) runtime errors. + /// + /// Otherwise this behaves exactly like `where_`. + /// + /// # Example + /// + /// ```ignore + /// Book::select() + /// .where_(Book::id.neq(&3)) + /// .where_raw("complex_function(book.title, ?, ?)", vec![&true, &"Foobar"]) + /// .await?; + /// ``` + pub fn where_raw( + self, + statement: impl Into, + params: Vec<&'a (dyn ToSql + Sync)>, + ) -> Update<'a, T> { + let where_ = Where::new(statement.into(), params); + + self.where_(where_) + } + /// Add a `SET` instruction to your `UPDATE` query. /// /// This function has to be called at least once before diff --git a/pg-worm/tests/connect.rs b/pg-worm/tests/connect.rs index 4450704..a338866 100644 --- a/pg-worm/tests/connect.rs +++ b/pg-worm/tests/connect.rs @@ -22,7 +22,9 @@ struct Author { #[tokio::test] async fn complete_procedure() -> Result<(), pg_worm::Error> { // First create a connection. This can be only done once. - Connection::to("postgres://postgres:postgres@localhost:5432").await?; + Connection::build("postgres://postgres:postgres@localhost:5432") + .max_pool_size(16) + .connect()?; // Then, create the tables for your models. // Use `register!` if you want to fail if a @@ -32,7 +34,7 @@ async fn complete_procedure() -> Result<(), pg_worm::Error> { // which is useful for development. // // If your tables already exist, skip this part. - force_register!(Author, Book)?; + force_create_table!(Author, Book).await?; // Next, insert some data. // This works by passing values for all