From cac2f25b8aa696df8b53de11ae818857f85689fe Mon Sep 17 00:00:00 2001 From: hanako-eo Date: Thu, 29 Aug 2024 15:07:08 +0200 Subject: [PATCH 01/11] Create a database crate to add query gestures and other stuff around the database --- Cargo.toml | 3 +++ crates/database/Cargo.toml | 8 ++++++++ crates/database/src/lib.rs | 5 +++++ crates/database/src/map.rs | 33 +++++++++++++++++++++++++++++++++ crates/database/src/query.rs | 23 +++++++++++++++++++++++ 5 files changed, 72 insertions(+) create mode 100644 crates/database/Cargo.toml create mode 100644 crates/database/src/lib.rs create mode 100644 crates/database/src/map.rs create mode 100644 crates/database/src/query.rs diff --git a/Cargo.toml b/Cargo.toml index a250128..7dc6c90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] +members = ["crates/database"] + [dependencies] actix-governor = "0.5" actix-web = "4.9" diff --git a/crates/database/Cargo.toml b/crates/database/Cargo.toml new file mode 100644 index 0000000..e6e36b8 --- /dev/null +++ b/crates/database/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "database" +version = "0.1.0" +edition = "2021" + +[dependencies] +deadpool-postgres = "0.14" +tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-uuid-1"] } diff --git a/crates/database/src/lib.rs b/crates/database/src/lib.rs new file mode 100644 index 0000000..ac7a0d1 --- /dev/null +++ b/crates/database/src/lib.rs @@ -0,0 +1,5 @@ +pub use map::ConstQueryMap; +pub use query::Query; + +mod map; +mod query; diff --git a/crates/database/src/map.rs b/crates/database/src/map.rs new file mode 100644 index 0000000..3470722 --- /dev/null +++ b/crates/database/src/map.rs @@ -0,0 +1,33 @@ +use core::borrow::Borrow; + +use super::query::Query; + +pub struct ConstQueryMap([(K, Query<'static>); N]); + +impl ConstQueryMap { + pub const fn new(queries: [(K, Query<'static>); N]) -> Self { + Self(queries) + } + + pub fn prepare(&self, k: Q) -> &Query + where + K: Borrow, + Q: PartialEq + { + self.try_prepare(k).expect("to have element") + } + + pub fn try_prepare(&self, k: Q) -> Option<&Query> + where + K: Borrow, + Q: PartialEq + { + for (key, query) in &self.0 { + if &k == key { + return Some(query); + } + } + + None + } +} diff --git a/crates/database/src/query.rs b/crates/database/src/query.rs new file mode 100644 index 0000000..e530744 --- /dev/null +++ b/crates/database/src/query.rs @@ -0,0 +1,23 @@ +use tokio_postgres::types::Type; + +pub struct Query<'q> (&'q str, &'q [Type]); + +impl<'q> Query<'q> { + #[inline] + pub fn params(query: &'q str, types: &'q [Type]) -> Self { + Self(query, types) + } + + #[inline] + pub fn new(query: &'q str) -> Self { + Self(query, &[]) + } + + pub fn query(&self) -> &'q str { + self.0 + } + + pub(crate) fn types(&self) -> &'q [Type] { + self.1 + } +} From b8fafabc5ad9f7d14ba0f7eae40832a5cf378bc4 Mon Sep 17 00:00:00 2001 From: hanako-eo Date: Thu, 29 Aug 2024 19:36:34 +0200 Subject: [PATCH 02/11] Add FromRow trait and derive macro and rename database crate to taom-database --- Cargo.toml | 3 +- crates/taom-database-macro/Cargo.toml | 12 +++++ crates/taom-database-macro/src/attribute.rs | 52 +++++++++++++++++++ crates/taom-database-macro/src/lib.rs | 52 +++++++++++++++++++ crates/{database => taom-database}/Cargo.toml | 3 +- crates/taom-database/src/from_row.rs | 7 +++ crates/{database => taom-database}/src/lib.rs | 2 + crates/{database => taom-database}/src/map.rs | 0 .../{database => taom-database}/src/query.rs | 0 9 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 crates/taom-database-macro/Cargo.toml create mode 100644 crates/taom-database-macro/src/attribute.rs create mode 100644 crates/taom-database-macro/src/lib.rs rename crates/{database => taom-database}/Cargo.toml (68%) create mode 100644 crates/taom-database/src/from_row.rs rename crates/{database => taom-database}/src/lib.rs (63%) rename crates/{database => taom-database}/src/map.rs (100%) rename crates/{database => taom-database}/src/query.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 7dc6c90..fdcc3c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = ["crates/database"] +members = ["crates/taom-database", "crates/taom-database-macro"] [dependencies] actix-governor = "0.5" @@ -29,6 +29,7 @@ semver = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = { version = "3.9", features = ["base64", "time_0_3"] } +taom-database = { path = "./crates/taom-database" } tokio = "1.39" tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-uuid-1"] } url = "2.5" diff --git a/crates/taom-database-macro/Cargo.toml b/crates/taom-database-macro/Cargo.toml new file mode 100644 index 0000000..71f88e5 --- /dev/null +++ b/crates/taom-database-macro/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "taom-database-macro" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2", features = ["full", "parsing"] } diff --git a/crates/taom-database-macro/src/attribute.rs b/crates/taom-database-macro/src/attribute.rs new file mode 100644 index 0000000..64f3544 --- /dev/null +++ b/crates/taom-database-macro/src/attribute.rs @@ -0,0 +1,52 @@ +use syn::{Attribute, LitStr, Result, Token}; + +macro_rules! fail { + ($t:expr, $m:expr) => { + return Err(syn::Error::new_spanned($t, $m)) + }; +} + +macro_rules! try_set { + (option : $i:expr, $v:expr, $t:expr) => { + match $i { + None => { $i = Some($v); Ok(()) }, + Some(_) => fail!($t, "duplicate attribute"), + } + }; + (bool : $i:expr, $t:expr) => { + match $i { + false => { $i = true; Ok(()) }, + true => fail!($t, "duplicate attribute"), + } + }; +} + +#[derive(Default)] +pub(crate) struct DbRowAttribute { + pub rename: Option, + pub default: bool, +} + +pub fn parse_db_row_attr(attrs: &[Attribute]) -> Result { + let mut result_attr = DbRowAttribute::default(); + + for attr in attrs.iter().filter(|attr| attr.path().is_ident("db_row")) { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("rename") { + meta.input.parse::()?; + let val: LitStr = meta.input.parse()?; + try_set!(option : result_attr.rename, val.value(), val) + } else if meta.path.is_ident("default") { + try_set!(bool : result_attr.default, meta.path) + } else { + let msg = match meta.path.get_ident() { + Some(ident) => format!("Unexpected attribute `{}` in db_row", ident), + None => "Unexpected attribute".to_string() + }; + fail!(meta.path, msg) + } + })? + } + + Ok(result_attr) +} diff --git a/crates/taom-database-macro/src/lib.rs b/crates/taom-database-macro/src/lib.rs new file mode 100644 index 0000000..1b038f5 --- /dev/null +++ b/crates/taom-database-macro/src/lib.rs @@ -0,0 +1,52 @@ +use attribute::parse_db_row_attr; +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput, Fields, Result}; + +mod attribute; + +#[proc_macro_derive(FromRow, attributes(db_row))] +pub fn derive_from_row(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let syn::Data::Struct(data) = input.data else { + return TokenStream::from( + syn::Error::new( + input.ident.span(), + "Only structs can derive `FromRow`" + ).to_compile_error() + ) + }; + + let struct_name = input.ident; + let fields = data.fields.iter().enumerate().map(|(i, field)| { + Ok(match field.ident.as_ref() { + Some(name) => { + let attr = parse_db_row_attr(field.attrs.as_slice())?; + let db_name = attr.rename.unwrap_or(name.to_string()); + quote! { #name: row.try_get(#db_name)? } + } + None => quote! { row.try_get(#i)? } + }) + }).collect::>>(); + + let fields = match fields { + Ok(ts) => ts, + Err(e) => return e.to_compile_error().into(), + }; + + let struct_self = match data.fields { + Fields::Named(_) => quote! { Self {#(#fields),*} }, + Fields::Unnamed(_) => quote! { Self(#(#fields),*) }, + Fields::Unit => quote! { Self }, + }; + + quote! { + #[automatically_derived] + impl ::taom_database::FromRow for #struct_name { + fn from_row(row: ::tokio_postgres::Row) -> Result { + Ok(#struct_self) + } + } + }.into() +} diff --git a/crates/database/Cargo.toml b/crates/taom-database/Cargo.toml similarity index 68% rename from crates/database/Cargo.toml rename to crates/taom-database/Cargo.toml index e6e36b8..01a9959 100644 --- a/crates/database/Cargo.toml +++ b/crates/taom-database/Cargo.toml @@ -1,8 +1,9 @@ [package] -name = "database" +name = "taom-database" version = "0.1.0" edition = "2021" [dependencies] +taom-database-macro = { path = "../taom-database-macro"} deadpool-postgres = "0.14" tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-uuid-1"] } diff --git a/crates/taom-database/src/from_row.rs b/crates/taom-database/src/from_row.rs new file mode 100644 index 0000000..d5dcfa2 --- /dev/null +++ b/crates/taom-database/src/from_row.rs @@ -0,0 +1,7 @@ +#[doc(hidden)] +pub use taom_database_macro::FromRow; +use tokio_postgres::{Error, Row}; + +pub trait FromRow: Sized { + fn from_row(row: Row) -> Result; +} diff --git a/crates/database/src/lib.rs b/crates/taom-database/src/lib.rs similarity index 63% rename from crates/database/src/lib.rs rename to crates/taom-database/src/lib.rs index ac7a0d1..da79983 100644 --- a/crates/database/src/lib.rs +++ b/crates/taom-database/src/lib.rs @@ -1,5 +1,7 @@ +pub use from_row::FromRow; pub use map::ConstQueryMap; pub use query::Query; +mod from_row; mod map; mod query; diff --git a/crates/database/src/map.rs b/crates/taom-database/src/map.rs similarity index 100% rename from crates/database/src/map.rs rename to crates/taom-database/src/map.rs diff --git a/crates/database/src/query.rs b/crates/taom-database/src/query.rs similarity index 100% rename from crates/database/src/query.rs rename to crates/taom-database/src/query.rs From cd8aac7afcad8c001da45829eb5705cb81089092 Mon Sep 17 00:00:00 2001 From: hanako-eo Date: Fri, 30 Aug 2024 00:00:15 +0200 Subject: [PATCH 03/11] Add impl for row and tuples --- crates/taom-database/src/from_row.rs | 202 +++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/crates/taom-database/src/from_row.rs b/crates/taom-database/src/from_row.rs index d5dcfa2..402cf56 100644 --- a/crates/taom-database/src/from_row.rs +++ b/crates/taom-database/src/from_row.rs @@ -1,7 +1,209 @@ #[doc(hidden)] pub use taom_database_macro::FromRow; +use tokio_postgres::types::FromSql; use tokio_postgres::{Error, Row}; pub trait FromRow: Sized { fn from_row(row: Row) -> Result; } + +impl FromRow for Row { + #[inline] + fn from_row(row: Row) -> Result { + Ok(row) + } +} + +macro_rules! impl_from_row_for_tuple { + () => { + impl FromRow for () { + #[inline] + fn from_row(_: Row) -> Result { + Ok(()) + } + } + }; + ($($idx:literal -> $T:ident;)+) => { + impl<$($T,)+> FromRow for ($($T,)+) + where + $($T: for<'r> FromSql<'r>,)+ + { + #[inline] + fn from_row(row: Row) -> Result { + Ok(($(row.try_get($idx as usize)?,)+)) + } + } + }; +} + + +impl_from_row_for_tuple! {} +impl_from_row_for_tuple! { + 0 -> T1; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; +} + +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; + 9 -> T10; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; + 9 -> T10; + 10 -> T11; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; + 9 -> T10; + 10 -> T11; + 11 -> T12; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; + 9 -> T10; + 10 -> T11; + 11 -> T12; + 12 -> T13; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; + 9 -> T10; + 10 -> T11; + 11 -> T12; + 12 -> T13; + 13 -> T14; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; + 9 -> T10; + 10 -> T11; + 11 -> T12; + 12 -> T13; + 13 -> T14; + 14 -> T15; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; + 9 -> T10; + 10 -> T11; + 11 -> T12; + 12 -> T13; + 13 -> T14; + 14 -> T15; + 15 -> T16; +} From 7ef9ad429f0d52d58bfd784aa1d791c72b9988ba Mon Sep 17 00:00:00 2001 From: hanako-eo Date: Fri, 30 Aug 2024 00:01:40 +0200 Subject: [PATCH 04/11] Add preparation of queries --- crates/taom-database/Cargo.toml | 3 ++- crates/taom-database/src/lib.rs | 5 +++++ crates/taom-database/src/map.rs | 25 +++++++++------------ crates/taom-database/src/prepare.rs | 35 +++++++++++++++++++++++++++++ crates/taom-database/src/query.rs | 3 +++ 5 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 crates/taom-database/src/prepare.rs diff --git a/crates/taom-database/Cargo.toml b/crates/taom-database/Cargo.toml index 01a9959..5c676a6 100644 --- a/crates/taom-database/Cargo.toml +++ b/crates/taom-database/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -taom-database-macro = { path = "../taom-database-macro"} deadpool-postgres = "0.14" +futures = "0.3" +taom-database-macro = { path = "../taom-database-macro"} tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-uuid-1"] } diff --git a/crates/taom-database/src/lib.rs b/crates/taom-database/src/lib.rs index da79983..8d2479d 100644 --- a/crates/taom-database/src/lib.rs +++ b/crates/taom-database/src/lib.rs @@ -1,7 +1,12 @@ +// allow to write `fn foo();` +// ^^^^^^^ +#![allow(invalid_type_param_default)] + pub use from_row::FromRow; pub use map::ConstQueryMap; pub use query::Query; mod from_row; mod map; +mod prepare; mod query; diff --git a/crates/taom-database/src/map.rs b/crates/taom-database/src/map.rs index 3470722..a9def2d 100644 --- a/crates/taom-database/src/map.rs +++ b/crates/taom-database/src/map.rs @@ -1,30 +1,27 @@ -use core::borrow::Borrow; +use tokio_postgres::Row; + +use crate::prepare::Prepare; +use crate::FromRow; use super::query::Query; pub struct ConstQueryMap([(K, Query<'static>); N]); +unsafe impl Sync for ConstQueryMap {} + impl ConstQueryMap { pub const fn new(queries: [(K, Query<'static>); N]) -> Self { Self(queries) } - pub fn prepare(&self, k: Q) -> &Query - where - K: Borrow, - Q: PartialEq - { - self.try_prepare(k).expect("to have element") + pub fn prepare(&self, k: K) -> Prepare { + self.try_prepare::(k).expect("item should exist") } - - pub fn try_prepare(&self, k: Q) -> Option<&Query> - where - K: Borrow, - Q: PartialEq - { + + pub fn try_prepare(&self, k: K) -> Option> { for (key, query) in &self.0 { if &k == key { - return Some(query); + return Some(Prepare::new(query.to_owned())); } } diff --git a/crates/taom-database/src/prepare.rs b/crates/taom-database/src/prepare.rs new file mode 100644 index 0000000..daccf4f --- /dev/null +++ b/crates/taom-database/src/prepare.rs @@ -0,0 +1,35 @@ +use core::marker::PhantomData; + +use deadpool_postgres::Client; +use futures::{Stream, StreamExt}; +use tokio_postgres::types::ToSql; +use tokio_postgres::{Error, Statement}; + +use crate::{FromRow, Query}; + +/// Prepare and send the request and allow to change the row value of each result +/// from the database with its generic `R` +pub struct Prepare { + query: Query<'static>, + _phantom: PhantomData +} + +impl Prepare { + pub fn new(query: Query<'static>) -> Self { + Self { + query, + _phantom: PhantomData + } + } + + async fn prepare(&self, client: &Client) -> Result { + client.prepare_typed_cached(self.query.query(), self.query.types()).await + } + + pub async fn query_iter(&self, client: Client, params: Vec<&(dyn ToSql+Sync)>) -> Result { + let statement = self.prepare(&client).await?; + let result = client.query_raw(&statement, params).await?; + + Ok(result.map(|row| R::from_row(row?))) + } +} diff --git a/crates/taom-database/src/query.rs b/crates/taom-database/src/query.rs index e530744..d7122f0 100644 --- a/crates/taom-database/src/query.rs +++ b/crates/taom-database/src/query.rs @@ -1,5 +1,8 @@ use tokio_postgres::types::Type; +/// Representation of sql query, +/// store the query string and the types of all argument like a big pointer. +#[derive(Clone)] pub struct Query<'q> (&'q str, &'q [Type]); impl<'q> Query<'q> { From 0e1e807496116e2c05a1cd28f3ae77941ec54456 Mon Sep 17 00:00:00 2001 From: hanako-eo Date: Fri, 30 Aug 2024 02:29:21 +0200 Subject: [PATCH 05/11] Generalize queries in `Prepare` --- crates/taom-database/src/lib.rs | 6 ++++ crates/taom-database/src/prepare.rs | 45 +++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/crates/taom-database/src/lib.rs b/crates/taom-database/src/lib.rs index 8d2479d..601d6c2 100644 --- a/crates/taom-database/src/lib.rs +++ b/crates/taom-database/src/lib.rs @@ -5,8 +5,14 @@ pub use from_row::FromRow; pub use map::ConstQueryMap; pub use query::Query; +use tokio_postgres::types::ToSql; mod from_row; mod map; mod prepare; mod query; + +#[inline(always)] +pub fn dynamic<'a, T: ToSql + Sync>(v: &'a T) -> &'a (dyn ToSql + Sync) { + v +} diff --git a/crates/taom-database/src/prepare.rs b/crates/taom-database/src/prepare.rs index daccf4f..6f3d99d 100644 --- a/crates/taom-database/src/prepare.rs +++ b/crates/taom-database/src/prepare.rs @@ -1,7 +1,7 @@ use core::marker::PhantomData; use deadpool_postgres::Client; -use futures::{Stream, StreamExt}; +use futures::{pin_mut, Stream, StreamExt, TryStreamExt}; use tokio_postgres::types::ToSql; use tokio_postgres::{Error, Statement}; @@ -22,14 +22,47 @@ impl Prepare { } } - async fn prepare(&self, client: &Client) -> Result { - client.prepare_typed_cached(self.query.query(), self.query.types()).await - } - - pub async fn query_iter(&self, client: Client, params: Vec<&(dyn ToSql+Sync)>) -> Result { + pub async fn query_iter<'a, P>(&self, client: Client, params: P) -> Result>, Error> + where + P: IntoIterator, + P::IntoIter: ExactSizeIterator + { let statement = self.prepare(&client).await?; let result = client.query_raw(&statement, params).await?; Ok(result.map(|row| R::from_row(row?))) } + + pub async fn query_one<'a, P>(&self, client: Client, params: P) -> Result, Error> + where + P: IntoIterator, + P::IntoIter: ExactSizeIterator, + { + let stream = self.query_iter(client, params).await?; + pin_mut!(stream); + + let row = match stream.try_next().await? { + Some(row) => row, + None => return Ok(None), + }; + + // if stream.try_next().await?.is_some() { + // return Err(InternalError::HasMoreThenOneRow(row)); + // } + + Ok(Some(row)) + } + + pub async fn execute<'a, P>(&self, client: Client, params: P) -> Result + where + P: IntoIterator, + P::IntoIter: ExactSizeIterator, + { + let statement = self.prepare(&client).await?; + client.execute_raw(&statement, params).await.map(|n| n as usize) + } + + async fn prepare(&self, client: &Client) -> Result { + client.prepare_typed_cached(self.query.query(), self.query.types()).await + } } From 503c4ffa803e20d5ee30dc64c9f05d5fde23ba14 Mon Sep 17 00:00:00 2001 From: hanako-eo Date: Fri, 30 Aug 2024 13:50:48 +0200 Subject: [PATCH 06/11] Add `ConfigBuilder` and `QueryError` in `Prepare` --- crates/taom-database/src/config.rs | 59 +++++++++++++++++++++++++++++ crates/taom-database/src/error.rs | 14 +++++++ crates/taom-database/src/lib.rs | 2 + crates/taom-database/src/prepare.rs | 50 +++++++++++++++--------- 4 files changed, 108 insertions(+), 17 deletions(-) create mode 100644 crates/taom-database/src/config.rs create mode 100644 crates/taom-database/src/error.rs diff --git a/crates/taom-database/src/config.rs b/crates/taom-database/src/config.rs new file mode 100644 index 0000000..74657dc --- /dev/null +++ b/crates/taom-database/src/config.rs @@ -0,0 +1,59 @@ +use deadpool_postgres::{Config, ManagerConfig, Pool, RecyclingMethod, Runtime}; +use tokio_postgres::NoTls; + +use crate::error::PoolError; + +#[derive(Default)] +pub struct ConfigBuilder<'b> { + host: Option<&'b str>, + password: Option<&'b str>, + user: Option<&'b str>, + database: Option<&'b str>, + recycling_method: RecyclingMethod, + runtime: Option +} + +impl<'b> ConfigBuilder<'b> { + pub fn host(mut self, host: &'b str) -> Self { + self.host = Some(host); + self + } + pub fn password(mut self, password: &'b str) -> Self { + self.password = Some(password); + self + } + pub fn user(mut self, user: &'b str) -> Self { + self.user = Some(user); + self + } + pub fn database(mut self, database: &'b str) -> Self { + self.database = Some(database); + self + } + pub fn recycling_method(mut self, recycling_method: RecyclingMethod) -> Self { + self.recycling_method = recycling_method; + self + } + pub fn runtime(mut self, runtime: Runtime) -> Self { + self.runtime = Some(runtime); + self + } + + pub async fn build(self) -> Result { + let mut pg_config = Config::new(); + pg_config.host = self.host.map(str::to_string); + pg_config.password = self.password.map(str::to_string); + pg_config.user = self.user.map(str::to_string); + pg_config.dbname = self.database.map(str::to_string); + pg_config.manager = Some(ManagerConfig { + recycling_method: self.recycling_method, + }); + + let pool = pg_config.create_pool(self.runtime, NoTls).map_err(|_| PoolError::Creation)?; + + // Try to connect to database to test if the database exist + let _ = pool.get().await.map_err(|_| PoolError::Connection)?; + + Ok(pool) + } +} diff --git a/crates/taom-database/src/error.rs b/crates/taom-database/src/error.rs new file mode 100644 index 0000000..9c8dcf5 --- /dev/null +++ b/crates/taom-database/src/error.rs @@ -0,0 +1,14 @@ +use tokio_postgres::Error; + +pub enum PoolError { + Creation, + Connection, +} + +pub enum QueryError { + PreparationFailed, + HasMoreThenOneRow(/*first_row:*/ R), + ExecuteFailed(Error), + HasNoRow, + GetRow, +} diff --git a/crates/taom-database/src/lib.rs b/crates/taom-database/src/lib.rs index 601d6c2..4ff7c1b 100644 --- a/crates/taom-database/src/lib.rs +++ b/crates/taom-database/src/lib.rs @@ -7,6 +7,8 @@ pub use map::ConstQueryMap; pub use query::Query; use tokio_postgres::types::ToSql; +pub mod config; +pub mod error; mod from_row; mod map; mod prepare; diff --git a/crates/taom-database/src/prepare.rs b/crates/taom-database/src/prepare.rs index 6f3d99d..638f5b0 100644 --- a/crates/taom-database/src/prepare.rs +++ b/crates/taom-database/src/prepare.rs @@ -5,6 +5,7 @@ use futures::{pin_mut, Stream, StreamExt, TryStreamExt}; use tokio_postgres::types::ToSql; use tokio_postgres::{Error, Statement}; +use crate::error::QueryError; use crate::{FromRow, Query}; /// Prepare and send the request and allow to change the row value of each result @@ -22,18 +23,26 @@ impl Prepare { } } - pub async fn query_iter<'a, P>(&self, client: Client, params: P) -> Result>, Error> + /// Return a Stream (it's like an async iterator) of all the row get by + /// the query or a `PreparationFailed`. + pub async fn query_iter<'a, P>(&self, client: Client, params: P) -> Result>, QueryError> where - P: IntoIterator, + P: IntoIterator, P::IntoIter: ExactSizeIterator - { + { let statement = self.prepare(&client).await?; - let result = client.query_raw(&statement, params).await?; - + let result = client.query_raw(&statement, params).await.map_err(|_| QueryError::PreparationFailed)?; + Ok(result.map(|row| R::from_row(row?))) } + - pub async fn query_one<'a, P>(&self, client: Client, params: P) -> Result, Error> + /// Return the first result of the query. + /// If the query result has only one row, it will be stored in `Ok(Some)`, + /// if the query has no result, it will return `Ok(None)`, + /// and if the query result has more than one row, the first row will be + /// stored in `Err(QueryError::HasMoreThenOneRow)`. + pub async fn query_one<'a, P>(&self, client: Client, params: P) -> Result, QueryError> where P: IntoIterator, P::IntoIter: ExactSizeIterator, @@ -41,28 +50,35 @@ impl Prepare { let stream = self.query_iter(client, params).await?; pin_mut!(stream); - let row = match stream.try_next().await? { - Some(row) => row, - None => return Ok(None), + let row = match stream.try_next().await { + Ok(Some(row)) => row, + Ok(None) => return Ok(None), + Err(_) => return Err(QueryError::GetRow) }; - - // if stream.try_next().await?.is_some() { - // return Err(InternalError::HasMoreThenOneRow(row)); - // } + + match stream.try_next().await { + Ok(Some(_)) | Err(_) => return Err(QueryError::HasMoreThenOneRow(row)), + Ok(None) => (), + } Ok(Some(row)) } - pub async fn execute<'a, P>(&self, client: Client, params: P) -> Result + /// Execute the query and return the amount of row update or `QueryError::ExecuteFailed` + /// in case of error. + pub async fn execute<'a, P>(&self, client: Client, params: P) -> Result> where P: IntoIterator, P::IntoIter: ExactSizeIterator, { let statement = self.prepare(&client).await?; - client.execute_raw(&statement, params).await.map(|n| n as usize) + match client.execute_raw(&statement, params).await { + Ok(n) => Ok(n as usize), + Err(err) => Err(QueryError::ExecuteFailed(err)) + } } - async fn prepare(&self, client: &Client) -> Result { - client.prepare_typed_cached(self.query.query(), self.query.types()).await + async fn prepare(&self, client: &Client) -> Result> { + client.prepare_typed_cached(self.query.query(), self.query.types()).await.map_err(|_| QueryError::PreparationFailed) } } From 6a292f68a0e562d6d17730d5e7bf95e04dd6bb8f Mon Sep 17 00:00:00 2001 From: hanako-eo Date: Fri, 30 Aug 2024 14:07:45 +0200 Subject: [PATCH 07/11] Format code --- crates/taom-database-macro/src/attribute.rs | 14 +++++-- crates/taom-database-macro/src/lib.rs | 36 ++++++++++-------- crates/taom-database/src/config.rs | 8 ++-- crates/taom-database/src/from_row.rs | 1 - crates/taom-database/src/map.rs | 6 +-- crates/taom-database/src/prepare.rs | 41 ++++++++++++++------- crates/taom-database/src/query.rs | 8 ++-- 7 files changed, 69 insertions(+), 45 deletions(-) diff --git a/crates/taom-database-macro/src/attribute.rs b/crates/taom-database-macro/src/attribute.rs index 64f3544..c98f59a 100644 --- a/crates/taom-database-macro/src/attribute.rs +++ b/crates/taom-database-macro/src/attribute.rs @@ -9,13 +9,19 @@ macro_rules! fail { macro_rules! try_set { (option : $i:expr, $v:expr, $t:expr) => { match $i { - None => { $i = Some($v); Ok(()) }, + None => { + $i = Some($v); + Ok(()) + } Some(_) => fail!($t, "duplicate attribute"), } }; (bool : $i:expr, $t:expr) => { match $i { - false => { $i = true; Ok(()) }, + false => { + $i = true; + Ok(()) + } true => fail!($t, "duplicate attribute"), } }; @@ -30,7 +36,7 @@ pub(crate) struct DbRowAttribute { pub fn parse_db_row_attr(attrs: &[Attribute]) -> Result { let mut result_attr = DbRowAttribute::default(); - for attr in attrs.iter().filter(|attr| attr.path().is_ident("db_row")) { + for attr in attrs.iter().filter(|attr| attr.path().is_ident("db_row")) { attr.parse_nested_meta(|meta| { if meta.path.is_ident("rename") { meta.input.parse::()?; @@ -41,7 +47,7 @@ pub fn parse_db_row_attr(attrs: &[Attribute]) -> Result { } else { let msg = match meta.path.get_ident() { Some(ident) => format!("Unexpected attribute `{}` in db_row", ident), - None => "Unexpected attribute".to_string() + None => "Unexpected attribute".to_string(), }; fail!(meta.path, msg) } diff --git a/crates/taom-database-macro/src/lib.rs b/crates/taom-database-macro/src/lib.rs index 1b038f5..bacf3b4 100644 --- a/crates/taom-database-macro/src/lib.rs +++ b/crates/taom-database-macro/src/lib.rs @@ -11,24 +11,27 @@ pub fn derive_from_row(input: TokenStream) -> TokenStream { let syn::Data::Struct(data) = input.data else { return TokenStream::from( - syn::Error::new( - input.ident.span(), - "Only structs can derive `FromRow`" - ).to_compile_error() - ) + syn::Error::new(input.ident.span(), "Only structs can derive `FromRow`") + .to_compile_error(), + ); }; let struct_name = input.ident; - let fields = data.fields.iter().enumerate().map(|(i, field)| { - Ok(match field.ident.as_ref() { - Some(name) => { - let attr = parse_db_row_attr(field.attrs.as_slice())?; - let db_name = attr.rename.unwrap_or(name.to_string()); - quote! { #name: row.try_get(#db_name)? } - } - None => quote! { row.try_get(#i)? } + let fields = data + .fields + .iter() + .enumerate() + .map(|(i, field)| { + Ok(match field.ident.as_ref() { + Some(name) => { + let attr = parse_db_row_attr(field.attrs.as_slice())?; + let db_name = attr.rename.unwrap_or(name.to_string()); + quote! { #name: row.try_get(#db_name)? } + } + None => quote! { row.try_get(#i)? }, + }) }) - }).collect::>>(); + .collect::>>(); let fields = match fields { Ok(ts) => ts, @@ -40,7 +43,7 @@ pub fn derive_from_row(input: TokenStream) -> TokenStream { Fields::Unnamed(_) => quote! { Self(#(#fields),*) }, Fields::Unit => quote! { Self }, }; - + quote! { #[automatically_derived] impl ::taom_database::FromRow for #struct_name { @@ -48,5 +51,6 @@ pub fn derive_from_row(input: TokenStream) -> TokenStream { Ok(#struct_self) } } - }.into() + } + .into() } diff --git a/crates/taom-database/src/config.rs b/crates/taom-database/src/config.rs index 74657dc..41562fe 100644 --- a/crates/taom-database/src/config.rs +++ b/crates/taom-database/src/config.rs @@ -10,7 +10,7 @@ pub struct ConfigBuilder<'b> { user: Option<&'b str>, database: Option<&'b str>, recycling_method: RecyclingMethod, - runtime: Option + runtime: Option, } impl<'b> ConfigBuilder<'b> { @@ -48,8 +48,10 @@ impl<'b> ConfigBuilder<'b> { pg_config.manager = Some(ManagerConfig { recycling_method: self.recycling_method, }); - - let pool = pg_config.create_pool(self.runtime, NoTls).map_err(|_| PoolError::Creation)?; + + let pool = pg_config + .create_pool(self.runtime, NoTls) + .map_err(|_| PoolError::Creation)?; // Try to connect to database to test if the database exist let _ = pool.get().await.map_err(|_| PoolError::Connection)?; diff --git a/crates/taom-database/src/from_row.rs b/crates/taom-database/src/from_row.rs index 402cf56..fdca314 100644 --- a/crates/taom-database/src/from_row.rs +++ b/crates/taom-database/src/from_row.rs @@ -36,7 +36,6 @@ macro_rules! impl_from_row_for_tuple { }; } - impl_from_row_for_tuple! {} impl_from_row_for_tuple! { 0 -> T1; diff --git a/crates/taom-database/src/map.rs b/crates/taom-database/src/map.rs index a9def2d..cde3a31 100644 --- a/crates/taom-database/src/map.rs +++ b/crates/taom-database/src/map.rs @@ -16,8 +16,8 @@ impl ConstQueryMap { pub fn prepare(&self, k: K) -> Prepare { self.try_prepare::(k).expect("item should exist") - } - + } + pub fn try_prepare(&self, k: K) -> Option> { for (key, query) in &self.0 { if &k == key { @@ -26,5 +26,5 @@ impl ConstQueryMap { } None - } + } } diff --git a/crates/taom-database/src/prepare.rs b/crates/taom-database/src/prepare.rs index 638f5b0..b9ac43b 100644 --- a/crates/taom-database/src/prepare.rs +++ b/crates/taom-database/src/prepare.rs @@ -12,37 +12,47 @@ use crate::{FromRow, Query}; /// from the database with its generic `R` pub struct Prepare { query: Query<'static>, - _phantom: PhantomData + _phantom: PhantomData, } impl Prepare { pub fn new(query: Query<'static>) -> Self { Self { query, - _phantom: PhantomData + _phantom: PhantomData, } } /// Return a Stream (it's like an async iterator) of all the row get by /// the query or a `PreparationFailed`. - pub async fn query_iter<'a, P>(&self, client: Client, params: P) -> Result>, QueryError> + pub async fn query_iter<'a, P>( + &self, + client: Client, + params: P, + ) -> Result>, QueryError> where - P: IntoIterator, - P::IntoIter: ExactSizeIterator - { + P: IntoIterator, + P::IntoIter: ExactSizeIterator, + { let statement = self.prepare(&client).await?; - let result = client.query_raw(&statement, params).await.map_err(|_| QueryError::PreparationFailed)?; - + let result = client + .query_raw(&statement, params) + .await + .map_err(|_| QueryError::PreparationFailed)?; + Ok(result.map(|row| R::from_row(row?))) } - /// Return the first result of the query. /// If the query result has only one row, it will be stored in `Ok(Some)`, /// if the query has no result, it will return `Ok(None)`, /// and if the query result has more than one row, the first row will be /// stored in `Err(QueryError::HasMoreThenOneRow)`. - pub async fn query_one<'a, P>(&self, client: Client, params: P) -> Result, QueryError> + pub async fn query_one<'a, P>( + &self, + client: Client, + params: P, + ) -> Result, QueryError> where P: IntoIterator, P::IntoIter: ExactSizeIterator, @@ -53,9 +63,9 @@ impl Prepare { let row = match stream.try_next().await { Ok(Some(row)) => row, Ok(None) => return Ok(None), - Err(_) => return Err(QueryError::GetRow) + Err(_) => return Err(QueryError::GetRow), }; - + match stream.try_next().await { Ok(Some(_)) | Err(_) => return Err(QueryError::HasMoreThenOneRow(row)), Ok(None) => (), @@ -74,11 +84,14 @@ impl Prepare { let statement = self.prepare(&client).await?; match client.execute_raw(&statement, params).await { Ok(n) => Ok(n as usize), - Err(err) => Err(QueryError::ExecuteFailed(err)) + Err(err) => Err(QueryError::ExecuteFailed(err)), } } async fn prepare(&self, client: &Client) -> Result> { - client.prepare_typed_cached(self.query.query(), self.query.types()).await.map_err(|_| QueryError::PreparationFailed) + client + .prepare_typed_cached(self.query.query(), self.query.types()) + .await + .map_err(|_| QueryError::PreparationFailed) } } diff --git a/crates/taom-database/src/query.rs b/crates/taom-database/src/query.rs index d7122f0..6ead908 100644 --- a/crates/taom-database/src/query.rs +++ b/crates/taom-database/src/query.rs @@ -3,16 +3,16 @@ use tokio_postgres::types::Type; /// Representation of sql query, /// store the query string and the types of all argument like a big pointer. #[derive(Clone)] -pub struct Query<'q> (&'q str, &'q [Type]); +pub struct Query<'q>(&'q str, &'q [Type]); impl<'q> Query<'q> { #[inline] - pub fn params(query: &'q str, types: &'q [Type]) -> Self { + pub const fn params(query: &'q str, types: &'q [Type]) -> Self { Self(query, types) } - + #[inline] - pub fn new(query: &'q str) -> Self { + pub const fn new(query: &'q str) -> Self { Self(query, &[]) } From 36604375e1e53087351b85c7e0fa587a722611b6 Mon Sep 17 00:00:00 2001 From: hanako-eo Date: Fri, 30 Aug 2024 16:50:02 +0200 Subject: [PATCH 08/11] Refactor source code to add queries and sql management with taom-database - Add `query_single` in `Prepare` - Impl `FromRow` of `String` and `int` types --- crates/taom-database/src/error.rs | 21 ++++++- crates/taom-database/src/from_row.rs | 15 +++++ crates/taom-database/src/prepare.rs | 43 +++++++++++--- crates/taom-database/src/query.rs | 15 +++++ src/database/mod.rs | 3 + src/database/queries.rs | 46 +++++++++++++++ src/errors/api.rs | 11 +++- src/main.rs | 50 ++++++---------- src/routes/connection.rs | 39 ++++--------- src/routes/game_server.rs | 36 +++++------- src/routes/players.rs | 86 ++++++++-------------------- 11 files changed, 204 insertions(+), 161 deletions(-) create mode 100644 src/database/mod.rs create mode 100644 src/database/queries.rs diff --git a/crates/taom-database/src/error.rs b/crates/taom-database/src/error.rs index 9c8dcf5..f702486 100644 --- a/crates/taom-database/src/error.rs +++ b/crates/taom-database/src/error.rs @@ -1,14 +1,29 @@ +use core::fmt::Display; + use tokio_postgres::Error; +use crate::Query; + pub enum PoolError { Creation, Connection, } +#[derive(Debug)] pub enum QueryError { - PreparationFailed, - HasMoreThenOneRow(/*first_row:*/ R), + PreparationFailed(Query<'static>), + HasMoreThanOneRow(/*first_row:*/ R), ExecuteFailed(Error), HasNoRow, - GetRow, +} + +impl Display for QueryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::PreparationFailed(_) => write!(f, "failure during the preparation of the query"), + Self::HasMoreThanOneRow(_) => write!(f, "collect more than one result"), + Self::ExecuteFailed(err) => write!(f, "{err}"), + Self::HasNoRow => write!(f, "no row found"), + } + } } diff --git a/crates/taom-database/src/from_row.rs b/crates/taom-database/src/from_row.rs index fdca314..036b403 100644 --- a/crates/taom-database/src/from_row.rs +++ b/crates/taom-database/src/from_row.rs @@ -14,6 +14,21 @@ impl FromRow for Row { } } +macro_rules! impl_from_row_for_from_sql { + ($($type:ty),+) => { + $( + impl FromRow for $type { + #[inline] + fn from_row(row: Row) -> Result { + row.try_get(0) + } + } + )+ + }; +} + +impl_from_row_for_from_sql!(i8, i16, i32, i64, String); + macro_rules! impl_from_row_for_tuple { () => { impl FromRow for () { diff --git a/crates/taom-database/src/prepare.rs b/crates/taom-database/src/prepare.rs index b9ac43b..d2ed1f8 100644 --- a/crates/taom-database/src/prepare.rs +++ b/crates/taom-database/src/prepare.rs @@ -27,7 +27,7 @@ impl Prepare { /// the query or a `PreparationFailed`. pub async fn query_iter<'a, P>( &self, - client: Client, + client: &Client, params: P, ) -> Result>, QueryError> where @@ -38,7 +38,7 @@ impl Prepare { let result = client .query_raw(&statement, params) .await - .map_err(|_| QueryError::PreparationFailed)?; + .map_err(|_| QueryError::PreparationFailed(self.query.clone()))?; Ok(result.map(|row| R::from_row(row?))) } @@ -47,10 +47,10 @@ impl Prepare { /// If the query result has only one row, it will be stored in `Ok(Some)`, /// if the query has no result, it will return `Ok(None)`, /// and if the query result has more than one row, the first row will be - /// stored in `Err(QueryError::HasMoreThenOneRow)`. + /// stored in `Err(QueryError::HasMoreThanOneRow)`. pub async fn query_one<'a, P>( &self, - client: Client, + client: &Client, params: P, ) -> Result, QueryError> where @@ -63,25 +63,50 @@ impl Prepare { let row = match stream.try_next().await { Ok(Some(row)) => row, Ok(None) => return Ok(None), - Err(_) => return Err(QueryError::GetRow), + Err(_) => return Err(QueryError::HasNoRow), }; match stream.try_next().await { - Ok(Some(_)) | Err(_) => return Err(QueryError::HasMoreThenOneRow(row)), + Ok(Some(_)) | Err(_) => return Err(QueryError::HasMoreThanOneRow(row)), Ok(None) => (), } Ok(Some(row)) } + /// Returns the only result of the query. + /// If the query has no result, it will return `Err(HasNoRow)`, but if + /// the query result has more than one row, the first row will be stored + /// in `Err(QueryError::HasMoreThanOneRow)`. + pub async fn query_single<'a, P>(&self, client: &Client, params: P) -> Result> + where + P: IntoIterator, + P::IntoIter: ExactSizeIterator, + { + let stream = self.query_iter(client, params).await?; + pin_mut!(stream); + + let row = match stream.try_next().await { + Ok(Some(row)) => row, + _ => return Err(QueryError::HasNoRow), + }; + + match stream.try_next().await { + Ok(Some(_)) | Err(_) => return Err(QueryError::HasMoreThanOneRow(row)), + Ok(None) => (), + } + + Ok(row) + } + /// Execute the query and return the amount of row update or `QueryError::ExecuteFailed` /// in case of error. - pub async fn execute<'a, P>(&self, client: Client, params: P) -> Result> + pub async fn execute<'a, P>(&self, client: &Client, params: P) -> Result> where P: IntoIterator, P::IntoIter: ExactSizeIterator, { - let statement = self.prepare(&client).await?; + let statement = self.prepare(client).await?; match client.execute_raw(&statement, params).await { Ok(n) => Ok(n as usize), Err(err) => Err(QueryError::ExecuteFailed(err)), @@ -92,6 +117,6 @@ impl Prepare { client .prepare_typed_cached(self.query.query(), self.query.types()) .await - .map_err(|_| QueryError::PreparationFailed) + .map_err(|_| QueryError::PreparationFailed(self.query.clone())) } } diff --git a/crates/taom-database/src/query.rs b/crates/taom-database/src/query.rs index 6ead908..6a8aeb8 100644 --- a/crates/taom-database/src/query.rs +++ b/crates/taom-database/src/query.rs @@ -1,3 +1,4 @@ +use std::fmt; use tokio_postgres::types::Type; /// Representation of sql query, @@ -24,3 +25,17 @@ impl<'q> Query<'q> { self.1 } } + +impl<'q> fmt::Display for Query<'q> { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.0, f) + } +} + +impl<'q> fmt::Debug for Query<'q> { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self.0, f) + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..626bfd4 --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,3 @@ +pub use queries::QUERIES; + +mod queries; diff --git a/src/database/queries.rs b/src/database/queries.rs new file mode 100644 index 0000000..c6bdf1a --- /dev/null +++ b/src/database/queries.rs @@ -0,0 +1,46 @@ +use taom_database::{ConstQueryMap, Query}; +use tokio_postgres::types::Type; + +pub const QUERIES: ConstQueryMap<&str, 9> = ConstQueryMap::new([ + // CONNECTION + ("find-player-info", Query::params( + "SELECT uuid, nickname FROM players WHERE id = $1", + &[Type::INT4] + )), + ("get-player-permissions", Query::params( + "SELECT permission FROM player_permissions WHERE player_id = $1", + &[Type::INT4] + )), + + // SHIPS + ("get-player-ship", Query::params( + "SELECT data FROM player_ships WHERE player_id = $1 AND slot = $2", + &[Type::INT4, Type::INT4] + )), + ("insert-player-ship", Query::params( + "INSERT INTO player_ships(player_id, slot, last_update, data) VALUES($1, $2, NOW(), $3) ON CONFLICT(player_id, slot) DO UPDATE SET last_update = NOW(), data = EXCLUDED.data", + &[Type::INT4, Type::INT4, Type::JSONB] + )), + + // PLAYERS + ("create-player", Query::params( + "INSERT INTO players(uuid, creation_time, nickname) VALUES($1, NOW(), $2) RETURNING id", + &[Type::UUID, Type::VARCHAR] + )), + ("create-token", Query::params( + "INSERT INTO player_tokens(token, player_id) VALUES($1, $2)", + &[Type::VARCHAR, Type::INT4] + )), + ("find-player-info", Query::params( + "SELECT uuid, nickname FROM players WHERE id = $1", + &[Type::INT4] + )), + ("find-token", Query::params( + "SELECT player_id FROM player_tokens WHERE token = $1", + &[Type::VARCHAR] + )), + ("update-player-connection", Query::params( + "UPDATE players SET last_connection_time = NOW() WHERE id = $1", + &[Type::INT4] + )), +]); diff --git a/src/errors/api.rs b/src/errors/api.rs index 2781449..178a869 100644 --- a/src/errors/api.rs +++ b/src/errors/api.rs @@ -153,8 +153,8 @@ impl ResponseError for RouteError { // to delete '$into_type:path' you need to use proc macros and further manipulation of the AST macro_rules! error_from { - (transform $from:path, $into_type:path, |$err_name:ident| $blk:block) => { - impl From<$from> for $into_type { + (transform$(<$T:ident>)? $from:path, $into_type:path, |$err_name:ident| $blk:block) => { + impl<$($T)?> From<$from> for $into_type { fn from($err_name: $from) -> Self { $blk } @@ -196,3 +196,10 @@ error_from! { transform jsonwebtoken::errors::Error, RouteError, |value| { ErrorCode::JWTAccident(value) ) } } + +error_from! { transform taom_database::error::QueryError, RouteError, |value| { + RouteError::ServerError( + ErrorCause::Database, + ErrorCode::External(value.to_string()) + ) +} } diff --git a/src/main.rs b/src/main.rs index 412b915..c86a281 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,10 @@ use actix_governor::{Governor, GovernorConfig, GovernorConfigBuilder}; use actix_web::{middleware, web, App, HttpServer}; use cached::TimedCache; use confy::ConfyError; +use deadpool_postgres::{RecyclingMethod, Runtime}; +use taom_database::config::ConfigBuilder; +use taom_database::error::PoolError; use tokio::sync::Mutex; -use tokio_postgres::NoTls; use crate::app_data::AppData; use crate::config::ApiConfig; @@ -15,6 +17,7 @@ use crate::fetcher::Fetcher; mod app_data; mod config; mod data; +mod database; mod deku_helper; mod errors; mod fetcher; @@ -24,26 +27,6 @@ mod routes; const CONFIG_FILE: Cow<'static, str> = Cow::Borrowed("tsom_api_config.toml"); -async fn setup_pg_pool(api_config: &ApiConfig) -> Result { - use deadpool_postgres::{Config, ManagerConfig, RecyclingMethod, Runtime}; - - let mut pg_config = Config::new(); - pg_config.host = Some(api_config.db_host.clone()); - pg_config.password = Some(api_config.db_password.unsecure().to_string()); - pg_config.user = Some(api_config.db_user.clone()); - pg_config.dbname = Some(api_config.db_database.clone()); - pg_config.manager = Some(ManagerConfig { - recycling_method: RecyclingMethod::Fast, - }); - - let pool = pg_config.create_pool(Some(Runtime::Tokio1), NoTls)?; - - // Try to connect to database to test if the database exist - let _ = pool.get().await?; - - Ok(pool) -} - #[actix_web::main] async fn main() -> Result<(), std::io::Error> { std::env::set_var("RUST_LOG", "info,actix_web=info"); @@ -72,19 +55,20 @@ async fn main() -> Result<(), std::io::Error> { let fetcher = Fetcher::from_config(&config).unwrap(); log::info!("Connection to the database"); - let pg_pool = match setup_pg_pool(&config).await { + let pg_pool = ConfigBuilder::default() + .host(config.db_host.as_str()) + .password(config.db_password.unsecure()) + .user(config.db_user.as_str()) + .database(config.db_database.as_str()) + .recycling_method(RecyclingMethod::Fast) + .runtime(Runtime::Tokio1) + .build() + .await; + + let pg_pool = match pg_pool { Ok(pool) => web::Data::new(pool), - Err(err) => { - use deadpool_postgres::{CreatePoolError, PoolError}; - - if err.is::() { - panic!("an error occured during the creation of the pool") - } else if err.is::() { - panic!("failed to connect to database") - } else { - unreachable!() - } - } + Err(PoolError::Creation) => panic!("an error occured during the creation of the pool"), + Err(PoolError::Connection) => panic!("failed to connect to database"), }; let bind_address = format!("{}:{}", config.listen_address, config.listen_port); diff --git a/src/routes/connection.rs b/src/routes/connection.rs index 696b398..1d4c303 100644 --- a/src/routes/connection.rs +++ b/src/routes/connection.rs @@ -1,15 +1,15 @@ use actix_web::{post, web, HttpResponse, Responder}; -use deadpool_postgres::tokio_postgres::types::Type; -use futures::{StreamExt, TryStreamExt}; +use futures::TryStreamExt; use jsonwebtoken::{EncodingKey, Header}; use serde::Deserialize; -use tokio_postgres::Row; +use taom_database::dynamic; use uuid::Uuid; use crate::config::ApiConfig; use crate::data::connection_token::{ConnectionToken, PrivateConnectionToken, ServerAddress}; use crate::data::game_data_token::GameDataToken; use crate::data::player_data::PlayerData; +use crate::database::QUERIES; use crate::errors::api::{ErrorCause, ErrorCode, RequestError, RouteError}; use crate::routes::players::validate_player_token; @@ -27,35 +27,19 @@ async fn game_connect( let pg_client = pg_pool.get().await?; let player_id = validate_player_token(&pg_client, ¶ms.token).await?; - // TODO(SirLynix): to do this with only one query - let find_player_info = pg_client - .prepare_typed_cached( - "SELECT uuid, nickname FROM players WHERE id = $1", - &[Type::INT4], - ) - .await?; - - let get_player_permissions = pg_client - .prepare_typed_cached( - "SELECT permission FROM player_permissions WHERE player_id = $1", - &[Type::INT4], - ) - .await?; - - let player_result = pg_client - .query_opt(&find_player_info, &[&player_id]) + let (uuid, nickname) = QUERIES + .prepare::<(Uuid, String)>("find-player-info") + .query_one(&pg_client, [dynamic(&player_id)]) .await? .ok_or(RouteError::InvalidRequest(RequestError::new( ErrorCode::AuthenticationInvalidToken, format!("No player has the id '{player_id}'"), )))?; - let uuid: Uuid = player_result.try_get(0)?; - let nickname: String = player_result.try_get(1)?; - let permissions: Vec = pg_client - .query_raw(&get_player_permissions, &[&player_id]) + let permissions: Vec = QUERIES + .prepare::("get-player-permissions") + .query_iter(&pg_client, [dynamic(&player_id)]) .await? - .map(|row: Result| row.and_then(|row| row.try_get(0))) .try_collect() .await?; @@ -84,10 +68,7 @@ async fn game_connect( server_address, private_token, ) - .map_err(|_| RouteError::ServerError( - ErrorCause::Internal, - ErrorCode::TokenGenerationFailed, - ))?; + .map_err(|_| RouteError::ServerError(ErrorCause::Internal, ErrorCode::TokenGenerationFailed))?; Ok(HttpResponse::Ok().json(token)) } diff --git a/src/routes/game_server.rs b/src/routes/game_server.rs index 6bd2744..c37a632 100644 --- a/src/routes/game_server.rs +++ b/src/routes/game_server.rs @@ -1,9 +1,10 @@ use actix_web::{get, patch, post, web, HttpRequest, HttpResponse, Responder}; use jsonwebtoken::{decode, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; -use tokio_postgres::types::Type; +use taom_database::{dynamic, FromRow}; use crate::data::game_data_token::GameDataToken; +use crate::database::QUERIES; use crate::errors::api::{ErrorCode, RequestError}; use crate::{config::ApiConfig, errors::api::RouteError}; @@ -105,8 +106,9 @@ async fn refresh_access_token( })) } -#[derive(Serialize)] +#[derive(FromRow, Serialize)] struct GetShipResponse { + #[db_row(rename = "data")] ship_data: serde_json::Value, } @@ -120,22 +122,15 @@ async fn player_ship_get( let access_token = validate_token(&req, &config, "access")?; let pg_client = pg_pool.get().await?; - let get_player_ship = pg_client - .prepare_typed_cached( - "SELECT data FROM player_ships WHERE player_id = $1 AND slot = $2", - &[Type::INT4, Type::INT4], - ) - .await?; - let row = pg_client - .query_opt(&get_player_ship, &[&access_token.player_db_id, &*path]) + let row = QUERIES + .prepare::("get-player-ship") + .query_one(&pg_client, [dynamic(&access_token.player_db_id), &*path]) .await?; Ok(match row { - Some(row) => HttpResponse::Ok().json(GetShipResponse { - ship_data: row.get(0), - }), - None => HttpResponse::NotFound().finish() + Some(response) => HttpResponse::Ok().json(response), + None => HttpResponse::NotFound().finish(), }) } @@ -155,17 +150,12 @@ async fn player_ship_patch( let access_token = validate_token(&req, &config, "access")?; let pg_client = pg_pool.get().await?; - let insert_player_ship = pg_client - .prepare_typed_cached( - "INSERT INTO player_ships(player_id, slot, last_update, data) VALUES($1, $2, NOW(), $3) ON CONFLICT(player_id, slot) DO UPDATE SET last_update = NOW(), data = EXCLUDED.data", - &[Type::INT4, Type::INT4, Type::JSONB], - ) - .await?; - pg_client + QUERIES + .prepare::<()>("insert-player-ship") .execute( - &insert_player_ship, - &[&access_token.player_db_id, &*path, ¶ms.data], + &pg_client, + [dynamic(&access_token.player_db_id), &*path, ¶ms.data], ) .await?; diff --git a/src/routes/players.rs b/src/routes/players.rs index 7e8e357..8291679 100644 --- a/src/routes/players.rs +++ b/src/routes/players.rs @@ -1,12 +1,13 @@ use actix_web::{post, web, HttpResponse, Responder}; -use deadpool_postgres::tokio_postgres::types::Type; use rand_core::OsRng; use serde::{Deserialize, Serialize}; +use taom_database::{dynamic, FromRow}; use uuid::Uuid; use crate::config::ApiConfig; use crate::data::token::Token; +use crate::database::QUERIES; use crate::errors::api::ErrorCause; use crate::errors::api::{ErrorCode, RequestError, RouteError}; @@ -60,21 +61,7 @@ async fn create( let uuid = Uuid::new_v4(); - let mut pg_client = pg_pool.get().await?; - - let create_player_statement = pg_client - .prepare_typed_cached( - "INSERT INTO players(uuid, creation_time, nickname) VALUES($1, NOW(), $2) RETURNING id", - &[Type::UUID, Type::VARCHAR], - ) - .await?; - - let create_token_statement = pg_client - .prepare_typed_cached( - "INSERT INTO player_tokens(token, player_id) VALUES($1, $2)", - &[Type::VARCHAR, Type::INT4], - ) - .await?; + let pg_client = pg_pool.get().await?; let Ok(token) = Token::generate(OsRng) else { return Err(RouteError::ServerError( @@ -83,18 +70,17 @@ async fn create( )); }; - let transaction = pg_client.transaction().await?; - let created_player_result = transaction - .query_one(&create_player_statement, &[&uuid, &nickname]) + // let transaction = pg_client.transaction().await?; + let player_id = QUERIES + .prepare::("create-player") + .query_single(&pg_client, [dynamic(&uuid), &nickname]) .await?; - let player_id: i32 = created_player_result.try_get(0)?; - - transaction - .execute(&create_token_statement, &[&token, &player_id]) + QUERIES + .prepare::<()>("create-token") + .execute(&pg_client, [dynamic(&token), &player_id]) .await?; - - transaction.commit().await?; + // transaction.commit().await?; Ok(HttpResponse::Ok().json(CreatePlayerResponse { uuid, token })) } @@ -104,7 +90,7 @@ struct AuthenticationParams { token: String, } -#[derive(Serialize)] +#[derive(FromRow, Serialize)] struct AuthenticationResponse { uuid: Uuid, nickname: String, @@ -118,15 +104,9 @@ async fn auth( let pg_client = pg_pool.get().await?; let player_id = validate_player_token(&pg_client, ¶ms.token).await?; - let find_player_info = pg_client - .prepare_typed_cached( - "SELECT uuid, nickname FROM players WHERE id = $1", - &[Type::INT4], - ) - .await?; - - let player_result = pg_client - .query_opt(&find_player_info, &[&player_id]) + let auth_response = QUERIES + .prepare::("find-play-info") + .query_one(&pg_client, [dynamic(&player_id)]) .await? .ok_or(RouteError::InvalidRequest(RequestError::new( ErrorCode::AuthenticationInvalidToken, @@ -136,10 +116,7 @@ async fn auth( // Update last connection time in a separate task as its result won't affect the route tokio::spawn(async move { update_player_connection(&pg_client, player_id).await }); - Ok(HttpResponse::Ok().json(AuthenticationResponse { - uuid: player_result.try_get(0)?, - nickname: player_result.try_get(1)?, - })) + Ok(HttpResponse::Ok().json(auth_response)) } pub async fn validate_player_token( @@ -160,39 +137,24 @@ pub async fn validate_player_token( ))); } - let find_token_statement = pg_client - .prepare_typed_cached( - "SELECT player_id FROM player_tokens WHERE token = $1", - &[Type::VARCHAR], - ) - .await?; - - let token_result = pg_client - .query_opt(&find_token_statement, &[&token]) + let player_id = QUERIES + .prepare::("find-token") + .query_one(pg_client, [dynamic(&token)]) .await? .ok_or(RouteError::InvalidRequest(RequestError::new( ErrorCode::AuthenticationInvalidToken, format!("No player has the token '{token}'"), )))?; - Ok(token_result.try_get(0)?) + Ok(player_id) } async fn update_player_connection(pg_client: &deadpool_postgres::Client, player_id: i32) { - match pg_client - .prepare_typed_cached( - "UPDATE players SET last_connection_time = NOW() WHERE id = $1", - &[Type::INT4], - ) + if let Err(err) = QUERIES + .prepare::<()>("update-player-connection") + .execute(pg_client, [dynamic(&player_id)]) .await { - Ok(statement) => { - if let Err(err) = pg_client.execute(&statement, &[&player_id]).await { - log::error!("Failed to update player {player_id} connection time: {err}"); - } - } - Err(err) => { - log::error!("Failed to update player {player_id} connection time (failed to prepare query): {err}"); - } + log::error!("Failed to update player {player_id} connection time: {err}"); } } From 824b2ea78f9aefdd18e4f9c4085a6953fa8a6134 Mon Sep 17 00:00:00 2001 From: hanako-eo Date: Sat, 31 Aug 2024 01:12:44 +0200 Subject: [PATCH 09/11] Move client for query/execution param to prepare --- crates/taom-database/src/map.rs | 9 ++--- crates/taom-database/src/prepare.rs | 56 +++++++++++++++-------------- src/routes/connection.rs | 8 ++--- src/routes/game_server.rs | 11 +++--- src/routes/players.rs | 20 +++++------ 5 files changed, 53 insertions(+), 51 deletions(-) diff --git a/crates/taom-database/src/map.rs b/crates/taom-database/src/map.rs index cde3a31..b06e87e 100644 --- a/crates/taom-database/src/map.rs +++ b/crates/taom-database/src/map.rs @@ -1,3 +1,4 @@ +use deadpool_postgres::Client; use tokio_postgres::Row; use crate::prepare::Prepare; @@ -14,14 +15,14 @@ impl ConstQueryMap { Self(queries) } - pub fn prepare(&self, k: K) -> Prepare { - self.try_prepare::(k).expect("item should exist") + pub fn prepare<'c, R: FromRow = Row>(&self, k: K, client: &'c Client) -> Prepare<'c, R> { + self.try_prepare::(k, client).expect("item should exist") } - pub fn try_prepare(&self, k: K) -> Option> { + pub fn try_prepare<'c, R: FromRow = Row>(&self, k: K, client: &'c Client) -> Option> { for (key, query) in &self.0 { if &k == key { - return Some(Prepare::new(query.to_owned())); + return Some(Prepare::new(client, query.to_owned())); } } diff --git a/crates/taom-database/src/prepare.rs b/crates/taom-database/src/prepare.rs index d2ed1f8..7a095ff 100644 --- a/crates/taom-database/src/prepare.rs +++ b/crates/taom-database/src/prepare.rs @@ -2,7 +2,7 @@ use core::marker::PhantomData; use deadpool_postgres::Client; use futures::{pin_mut, Stream, StreamExt, TryStreamExt}; -use tokio_postgres::types::ToSql; +use tokio_postgres::types::BorrowToSql; use tokio_postgres::{Error, Statement}; use crate::error::QueryError; @@ -10,32 +10,34 @@ use crate::{FromRow, Query}; /// Prepare and send the request and allow to change the row value of each result /// from the database with its generic `R` -pub struct Prepare { +pub struct Prepare<'c, R> { query: Query<'static>, + client: &'c Client, _phantom: PhantomData, } -impl Prepare { - pub fn new(query: Query<'static>) -> Self { +impl<'c, R: FromRow> Prepare<'c, R> { + pub fn new(client: &'c Client, query: Query<'static>) -> Self { Self { query, + client, _phantom: PhantomData, } } /// Return a Stream (it's like an async iterator) of all the row get by /// the query or a `PreparationFailed`. - pub async fn query_iter<'a, P>( + pub async fn query_iter( &self, - client: &Client, params: P, ) -> Result>, QueryError> where - P: IntoIterator, - P::IntoIter: ExactSizeIterator, + I: BorrowToSql, + P: IntoIterator, + P::IntoIter: ExactSizeIterator, { - let statement = self.prepare(&client).await?; - let result = client + let statement = self.prepare().await?; + let result = self.client .query_raw(&statement, params) .await .map_err(|_| QueryError::PreparationFailed(self.query.clone()))?; @@ -48,16 +50,16 @@ impl Prepare { /// if the query has no result, it will return `Ok(None)`, /// and if the query result has more than one row, the first row will be /// stored in `Err(QueryError::HasMoreThanOneRow)`. - pub async fn query_one<'a, P>( + pub async fn query_one( &self, - client: &Client, params: P, ) -> Result, QueryError> where - P: IntoIterator, - P::IntoIter: ExactSizeIterator, + I: BorrowToSql, + P: IntoIterator, + P::IntoIter: ExactSizeIterator, { - let stream = self.query_iter(client, params).await?; + let stream = self.query_iter(params).await?; pin_mut!(stream); let row = match stream.try_next().await { @@ -78,12 +80,13 @@ impl Prepare { /// If the query has no result, it will return `Err(HasNoRow)`, but if /// the query result has more than one row, the first row will be stored /// in `Err(QueryError::HasMoreThanOneRow)`. - pub async fn query_single<'a, P>(&self, client: &Client, params: P) -> Result> + pub async fn query_single(&self, params: P) -> Result> where - P: IntoIterator, - P::IntoIter: ExactSizeIterator, + I: BorrowToSql, + P: IntoIterator, + P::IntoIter: ExactSizeIterator, { - let stream = self.query_iter(client, params).await?; + let stream = self.query_iter(params).await?; pin_mut!(stream); let row = match stream.try_next().await { @@ -101,20 +104,21 @@ impl Prepare { /// Execute the query and return the amount of row update or `QueryError::ExecuteFailed` /// in case of error. - pub async fn execute<'a, P>(&self, client: &Client, params: P) -> Result> + pub async fn execute(&self, params: P) -> Result> where - P: IntoIterator, - P::IntoIter: ExactSizeIterator, + I: BorrowToSql, + P: IntoIterator, + P::IntoIter: ExactSizeIterator, { - let statement = self.prepare(client).await?; - match client.execute_raw(&statement, params).await { + let statement = self.prepare().await?; + match self.client.execute_raw(&statement, params).await { Ok(n) => Ok(n as usize), Err(err) => Err(QueryError::ExecuteFailed(err)), } } - async fn prepare(&self, client: &Client) -> Result> { - client + async fn prepare(&self) -> Result> { + self.client .prepare_typed_cached(self.query.query(), self.query.types()) .await .map_err(|_| QueryError::PreparationFailed(self.query.clone())) diff --git a/src/routes/connection.rs b/src/routes/connection.rs index 1d4c303..65e941f 100644 --- a/src/routes/connection.rs +++ b/src/routes/connection.rs @@ -28,8 +28,8 @@ async fn game_connect( let player_id = validate_player_token(&pg_client, ¶ms.token).await?; let (uuid, nickname) = QUERIES - .prepare::<(Uuid, String)>("find-player-info") - .query_one(&pg_client, [dynamic(&player_id)]) + .prepare::<(Uuid, String)>("find-player-info", &pg_client) + .query_one([dynamic(&player_id)]) .await? .ok_or(RouteError::InvalidRequest(RequestError::new( ErrorCode::AuthenticationInvalidToken, @@ -37,8 +37,8 @@ async fn game_connect( )))?; let permissions: Vec = QUERIES - .prepare::("get-player-permissions") - .query_iter(&pg_client, [dynamic(&player_id)]) + .prepare::("get-player-permissions", &pg_client) + .query_iter([dynamic(&player_id)]) .await? .try_collect() .await?; diff --git a/src/routes/game_server.rs b/src/routes/game_server.rs index c37a632..76bff35 100644 --- a/src/routes/game_server.rs +++ b/src/routes/game_server.rs @@ -124,8 +124,8 @@ async fn player_ship_get( let pg_client = pg_pool.get().await?; let row = QUERIES - .prepare::("get-player-ship") - .query_one(&pg_client, [dynamic(&access_token.player_db_id), &*path]) + .prepare::("get-player-ship", &pg_client) + .query_one([dynamic(&access_token.player_db_id), &*path]) .await?; Ok(match row { @@ -152,11 +152,8 @@ async fn player_ship_patch( let pg_client = pg_pool.get().await?; QUERIES - .prepare::<()>("insert-player-ship") - .execute( - &pg_client, - [dynamic(&access_token.player_db_id), &*path, ¶ms.data], - ) + .prepare::<()>("insert-player-ship", &pg_client) + .execute([dynamic(&access_token.player_db_id), &*path, ¶ms.data]) .await?; Ok(HttpResponse::Ok().finish()) diff --git a/src/routes/players.rs b/src/routes/players.rs index 8291679..6427077 100644 --- a/src/routes/players.rs +++ b/src/routes/players.rs @@ -72,13 +72,13 @@ async fn create( // let transaction = pg_client.transaction().await?; let player_id = QUERIES - .prepare::("create-player") - .query_single(&pg_client, [dynamic(&uuid), &nickname]) + .prepare::("create-player", &pg_client) + .query_single([dynamic(&uuid), &nickname]) .await?; QUERIES - .prepare::<()>("create-token") - .execute(&pg_client, [dynamic(&token), &player_id]) + .prepare::<()>("create-token", &pg_client) + .execute([dynamic(&token), &player_id]) .await?; // transaction.commit().await?; @@ -105,8 +105,8 @@ async fn auth( let player_id = validate_player_token(&pg_client, ¶ms.token).await?; let auth_response = QUERIES - .prepare::("find-play-info") - .query_one(&pg_client, [dynamic(&player_id)]) + .prepare::("find-play-info", &pg_client) + .query_one([dynamic(&player_id)]) .await? .ok_or(RouteError::InvalidRequest(RequestError::new( ErrorCode::AuthenticationInvalidToken, @@ -138,8 +138,8 @@ pub async fn validate_player_token( } let player_id = QUERIES - .prepare::("find-token") - .query_one(pg_client, [dynamic(&token)]) + .prepare::("find-token", pg_client) + .query_one([dynamic(&token)]) .await? .ok_or(RouteError::InvalidRequest(RequestError::new( ErrorCode::AuthenticationInvalidToken, @@ -151,8 +151,8 @@ pub async fn validate_player_token( async fn update_player_connection(pg_client: &deadpool_postgres::Client, player_id: i32) { if let Err(err) = QUERIES - .prepare::<()>("update-player-connection") - .execute(pg_client, [dynamic(&player_id)]) + .prepare::<()>("update-player-connection", pg_client) + .execute([dynamic(&player_id)]) .await { log::error!("Failed to update player {player_id} connection time: {err}"); From 8c35a09f5aa7681b10424c35a41b0476022e6c59 Mon Sep 17 00:00:00 2001 From: hanako-eo Date: Sat, 31 Aug 2024 22:43:45 +0200 Subject: [PATCH 10/11] Format and add few comment + join 2 queries in connection --- crates/taom-database/src/from_row.rs | 1 + crates/taom-database/src/map.rs | 9 +++++- crates/taom-database/src/prepare.rs | 8 ++---- src/routes/connection.rs | 43 +++++++++++++++++----------- src/routes/players.rs | 10 +++---- 5 files changed, 43 insertions(+), 28 deletions(-) diff --git a/crates/taom-database/src/from_row.rs b/crates/taom-database/src/from_row.rs index 036b403..0796c2f 100644 --- a/crates/taom-database/src/from_row.rs +++ b/crates/taom-database/src/from_row.rs @@ -3,6 +3,7 @@ pub use taom_database_macro::FromRow; use tokio_postgres::types::FromSql; use tokio_postgres::{Error, Row}; +/// Trait to transform a row into an object pub trait FromRow: Sized { fn from_row(row: Row) -> Result; } diff --git a/crates/taom-database/src/map.rs b/crates/taom-database/src/map.rs index b06e87e..2102c19 100644 --- a/crates/taom-database/src/map.rs +++ b/crates/taom-database/src/map.rs @@ -6,6 +6,7 @@ use crate::FromRow; use super::query::Query; +/// Constante map of all queries pub struct ConstQueryMap([(K, Query<'static>); N]); unsafe impl Sync for ConstQueryMap {} @@ -15,11 +16,17 @@ impl ConstQueryMap { Self(queries) } + /// Give Prepare object to prepare a query pub fn prepare<'c, R: FromRow = Row>(&self, k: K, client: &'c Client) -> Prepare<'c, R> { self.try_prepare::(k, client).expect("item should exist") } - pub fn try_prepare<'c, R: FromRow = Row>(&self, k: K, client: &'c Client) -> Option> { + /// Try to give Prepare object to prepare a query + pub fn try_prepare<'c, R: FromRow = Row>( + &self, + k: K, + client: &'c Client, + ) -> Option> { for (key, query) in &self.0 { if &k == key { return Some(Prepare::new(client, query.to_owned())); diff --git a/crates/taom-database/src/prepare.rs b/crates/taom-database/src/prepare.rs index 7a095ff..56b7636 100644 --- a/crates/taom-database/src/prepare.rs +++ b/crates/taom-database/src/prepare.rs @@ -37,7 +37,8 @@ impl<'c, R: FromRow> Prepare<'c, R> { P::IntoIter: ExactSizeIterator, { let statement = self.prepare().await?; - let result = self.client + let result = self + .client .query_raw(&statement, params) .await .map_err(|_| QueryError::PreparationFailed(self.query.clone()))?; @@ -50,10 +51,7 @@ impl<'c, R: FromRow> Prepare<'c, R> { /// if the query has no result, it will return `Ok(None)`, /// and if the query result has more than one row, the first row will be /// stored in `Err(QueryError::HasMoreThanOneRow)`. - pub async fn query_one( - &self, - params: P, - ) -> Result, QueryError> + pub async fn query_one(&self, params: P) -> Result, QueryError> where I: BorrowToSql, P: IntoIterator, diff --git a/src/routes/connection.rs b/src/routes/connection.rs index 65e941f..25a0175 100644 --- a/src/routes/connection.rs +++ b/src/routes/connection.rs @@ -1,4 +1,5 @@ use actix_web::{post, web, HttpResponse, Responder}; +use futures::future::join; use futures::TryStreamExt; use jsonwebtoken::{EncodingKey, Header}; use serde::Deserialize; @@ -27,23 +28,33 @@ async fn game_connect( let pg_client = pg_pool.get().await?; let player_id = validate_player_token(&pg_client, ¶ms.token).await?; - let (uuid, nickname) = QUERIES - .prepare::<(Uuid, String)>("find-player-info", &pg_client) - .query_one([dynamic(&player_id)]) - .await? - .ok_or(RouteError::InvalidRequest(RequestError::new( - ErrorCode::AuthenticationInvalidToken, - format!("No player has the id '{player_id}'"), - )))?; - - let permissions: Vec = QUERIES - .prepare::("get-player-permissions", &pg_client) - .query_iter([dynamic(&player_id)]) - .await? - .try_collect() - .await?; + let queries = join( + async { + Ok(QUERIES + .prepare::<(Uuid, String)>("find-player-info", &pg_client) + .query_one([dynamic(&player_id)]) + .await? + .ok_or(RouteError::InvalidRequest(RequestError::new( + ErrorCode::AuthenticationInvalidToken, + format!("No player has the id '{player_id}'"), + )))?) + }, + async { + Ok(QUERIES + .prepare::("get-player-permissions", &pg_client) + .query_iter([dynamic(&player_id)]) + .await? + .try_collect::>() + .await?) + }, + ); - let player_data = PlayerData::new(uuid, nickname, permissions); + let (uuid, player_data) = match queries.await { + (Ok((uuid, nickname)), Ok(permissions)) => { + (uuid, PlayerData::new(uuid, nickname, permissions)) + } + (Err(err), _) | (_, Err(err)) => return Err(err), + }; let server_address = ServerAddress::new(config.game_server_address.as_str(), config.game_server_port); diff --git a/src/routes/players.rs b/src/routes/players.rs index 6427077..27907a3 100644 --- a/src/routes/players.rs +++ b/src/routes/players.rs @@ -59,10 +59,6 @@ async fn create( } } - let uuid = Uuid::new_v4(); - - let pg_client = pg_pool.get().await?; - let Ok(token) = Token::generate(OsRng) else { return Err(RouteError::ServerError( ErrorCause::Internal, @@ -70,7 +66,10 @@ async fn create( )); }; - // let transaction = pg_client.transaction().await?; + let uuid = Uuid::new_v4(); + + let pg_client = pg_pool.get().await?; + let player_id = QUERIES .prepare::("create-player", &pg_client) .query_single([dynamic(&uuid), &nickname]) @@ -80,7 +79,6 @@ async fn create( .prepare::<()>("create-token", &pg_client) .execute([dynamic(&token), &player_id]) .await?; - // transaction.commit().await?; Ok(HttpResponse::Ok().json(CreatePlayerResponse { uuid, token })) } From 7c553cf00812039d555cf55faae8f24828b2f393 Mon Sep 17 00:00:00 2001 From: hanako-eo Date: Sat, 31 Aug 2024 23:16:44 +0200 Subject: [PATCH 11/11] Fix db_row attribute --- crates/taom-database-macro/src/lib.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/taom-database-macro/src/lib.rs b/crates/taom-database-macro/src/lib.rs index bacf3b4..3063131 100644 --- a/crates/taom-database-macro/src/lib.rs +++ b/crates/taom-database-macro/src/lib.rs @@ -22,13 +22,21 @@ pub fn derive_from_row(input: TokenStream) -> TokenStream { .iter() .enumerate() .map(|(i, field)| { + let attr = parse_db_row_attr(field.attrs.as_slice())?; Ok(match field.ident.as_ref() { Some(name) => { - let attr = parse_db_row_attr(field.attrs.as_slice())?; let db_name = attr.rename.unwrap_or(name.to_string()); - quote! { #name: row.try_get(#db_name)? } + match attr.default { + true => quote! { #name: row.try_get(#db_name).unwrap_or_default() }, + false => quote! { #name: row.try_get(#db_name)? }, + } + } + None => match (attr.rename, attr.default) { + (Some(db_name), true) => quote! { row.try_get(#db_name).unwrap_or_default() }, + (Some(db_name), false) => quote! { row.try_get(#db_name)? }, + (None, true) => quote! { row.try_get(#i).unwrap_or_default() }, + (None, false) => quote! { row.try_get(#i)? }, } - None => quote! { row.try_get(#i)? }, }) }) .collect::>>();