fix: simple ORM for one struct and foreign references work
Currently, all methods declared in the Georm trait are available. If a struct has an ID pointing towards another entity, the user can create a get method to get the entity pointed at from the database too (local one-to-one relationship). I still need to implement remote one-to-one relationships (one-to-one relationships when the ID of the remote object is not available locally). I still need to also test and debug one-to-many relationships (ID of the remote entiies not available locally) and many-to-many relationships (declared in a dedicated table). For now, IDs in all cases are simple types recognized by SQLx that are not arrays. Options are only supported when explicitely specified for one-to-one relationships.
This commit is contained in:
parent
96ac2aa979
commit
bca0619f30
@ -1,7 +0,0 @@
|
|||||||
[all]
|
|
||||||
out = ["Xml"]
|
|
||||||
target-dir = "coverage"
|
|
||||||
output-dir = "coverage"
|
|
||||||
fail-under = 40
|
|
||||||
exclude-files = ["target/*"]
|
|
||||||
run-types = ["AllTargets"]
|
|
@ -1,8 +0,0 @@
|
|||||||
[all]
|
|
||||||
out = ["Html", "Lcov"]
|
|
||||||
skip-clean = true
|
|
||||||
target-dir = "coverage"
|
|
||||||
output-dir = "coverage"
|
|
||||||
fail-under = 40
|
|
||||||
exclude-files = ["target/*"]
|
|
||||||
run-types = ["AllTargets"]
|
|
110
Cargo.lock
generated
110
Cargo.lock
generated
@ -418,6 +418,7 @@ name = "georm"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"georm-macros",
|
"georm-macros",
|
||||||
|
"rand 0.9.0",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -439,7 +440,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi 0.13.3+wasi-0.2.2",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -765,7 +778,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -781,7 +794,7 @@ dependencies = [
|
|||||||
"num-integer",
|
"num-integer",
|
||||||
"num-iter",
|
"num-iter",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand",
|
"rand 0.8.5",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@ -920,7 +933,7 @@ version = "0.2.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy",
|
"zerocopy 0.7.35",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -958,8 +971,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha",
|
"rand_chacha 0.3.1",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"rand_core 0.9.0",
|
||||||
|
"zerocopy 0.8.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -969,7 +993,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -978,7 +1012,17 @@ version = "0.6.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.15",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.1",
|
||||||
|
"zerocopy 0.8.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1003,7 +1047,7 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
"pkcs1",
|
"pkcs1",
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
"signature",
|
"signature",
|
||||||
"spki",
|
"spki",
|
||||||
"subtle",
|
"subtle",
|
||||||
@ -1114,7 +1158,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1276,7 +1320,7 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rand",
|
"rand 0.8.5",
|
||||||
"rsa",
|
"rsa",
|
||||||
"sha1",
|
"sha1",
|
||||||
"sha2",
|
"sha2",
|
||||||
@ -1313,7 +1357,7 @@ dependencies = [
|
|||||||
"md-5",
|
"md-5",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
@ -1406,7 +1450,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom",
|
"getrandom 0.2.15",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
@ -1606,6 +1650,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.13.3+wasi-0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen-rt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasite"
|
name = "wasite"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -1779,6 +1832,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rt"
|
||||||
|
version = "0.33.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "write16"
|
name = "write16"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@ -1822,7 +1884,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"zerocopy-derive",
|
"zerocopy-derive 0.7.35",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive 0.8.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1836,6 +1907,17 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
@ -34,6 +34,9 @@ features = ["postgres", "runtime-tokio", "macros", "migrate"]
|
|||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
georm-macros = { workspace = true }
|
georm-macros = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rand = "0.9"
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
unsafe_code = "forbid"
|
unsafe_code = "forbid"
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
[licenses]
|
[licenses]
|
||||||
# If there is a need to add another license, please refer to this
|
# If there is a need to add another license, please refer to this page
|
||||||
# page: https://www.gnu.org/licenses/license-list.html
|
# for compatible licenses:
|
||||||
|
# https://www.gnu.org/licenses/license-list.html
|
||||||
allow = ["MIT", "Apache-2.0", "BSD-3-Clause", "Unicode-3.0", "Zlib"]
|
allow = ["MIT", "Apache-2.0", "BSD-3-Clause", "Unicode-3.0", "Zlib"]
|
||||||
confidence-threshold = 0.8
|
confidence-threshold = 0.8
|
||||||
|
|
||||||
[bans]
|
[bans]
|
||||||
multiple-versions = "warn"
|
multiple-versions = "allow"
|
||||||
wildcards = "allow"
|
wildcards = "allow"
|
||||||
highlight = "all"
|
highlight = "all"
|
||||||
workspace-default-features = "allow"
|
workspace-default-features = "allow"
|
||||||
|
@ -39,7 +39,6 @@ SQLX_OFFLINE="1" cargo build --release
|
|||||||
bacon
|
bacon
|
||||||
cargo
|
cargo
|
||||||
cargo-deny
|
cargo-deny
|
||||||
cargo-tarpaulin
|
|
||||||
just
|
just
|
||||||
rust-analyzer
|
rust-analyzer
|
||||||
(rustVersion.override {
|
(rustVersion.override {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
use quote::quote;
|
use quote::quote;
|
||||||
use std::fmt::{self, Display};
|
|
||||||
|
|
||||||
#[derive(deluxe::ExtractAttributes)]
|
#[derive(deluxe::ExtractAttributes)]
|
||||||
#[deluxe(attributes(georm))]
|
#[deluxe(attributes(georm))]
|
||||||
@ -32,7 +31,7 @@ impl From<&O2MRelationship> for proc_macro2::TokenStream {
|
|||||||
);
|
);
|
||||||
quote! {
|
quote! {
|
||||||
pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Vec<#entity>> {
|
pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Vec<#entity>> {
|
||||||
query_as!(#entity, #query, self.get_id()).fetch_all(pool).await
|
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,7 +121,7 @@ WHERE local.{} = $1
|
|||||||
);
|
);
|
||||||
quote! {
|
quote! {
|
||||||
pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Vec<#entity>> {
|
pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Vec<#entity>> {
|
||||||
query_as!(#entity, #query, self.get_id()).fetch_all(pool).await
|
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,16 +133,11 @@ struct GeormFieldAttributes {
|
|||||||
#[deluxe(default = false)]
|
#[deluxe(default = false)]
|
||||||
pub id: bool,
|
pub id: bool,
|
||||||
#[deluxe(default = None)]
|
#[deluxe(default = None)]
|
||||||
pub column: Option<String>,
|
|
||||||
#[deluxe(default = None)]
|
|
||||||
pub relation: Option<O2ORelationship>,
|
pub relation: Option<O2ORelationship>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[georm(
|
// #[georm(relation = { name = profile, id = "id", entity = Profile, nullable })]
|
||||||
// table = "profileId",
|
#[derive(deluxe::ParseMetaItem, Clone, Debug)]
|
||||||
// one_to_one = { name = profile, id = "id", entity = Profile, nullable }
|
|
||||||
// )]
|
|
||||||
#[derive(deluxe::ParseMetaItem, Clone)]
|
|
||||||
pub struct O2ORelationship {
|
pub struct O2ORelationship {
|
||||||
pub entity: syn::Type,
|
pub entity: syn::Type,
|
||||||
pub table: String,
|
pub table: String,
|
||||||
@ -154,12 +148,11 @@ pub struct O2ORelationship {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct GeormField {
|
pub struct GeormField {
|
||||||
pub ident: syn::Ident,
|
pub ident: syn::Ident,
|
||||||
pub field: syn::Field,
|
pub field: syn::Field,
|
||||||
pub ty: syn::Type,
|
pub ty: syn::Type,
|
||||||
pub column: Option<String>,
|
|
||||||
pub id: bool,
|
pub id: bool,
|
||||||
pub relation: Option<O2ORelationship>,
|
pub relation: Option<O2ORelationship>,
|
||||||
}
|
}
|
||||||
@ -170,40 +163,22 @@ impl GeormField {
|
|||||||
let ty = field.clone().ty;
|
let ty = field.clone().ty;
|
||||||
let attrs: GeormFieldAttributes =
|
let attrs: GeormFieldAttributes =
|
||||||
deluxe::extract_attributes(field).expect("Could not extract attributes from field");
|
deluxe::extract_attributes(field).expect("Could not extract attributes from field");
|
||||||
let GeormFieldAttributes {
|
let GeormFieldAttributes { id, relation } = attrs;
|
||||||
id,
|
|
||||||
column,
|
|
||||||
relation,
|
|
||||||
} = attrs;
|
|
||||||
Self {
|
Self {
|
||||||
ident,
|
ident,
|
||||||
field: field.to_owned(),
|
field: field.to_owned(),
|
||||||
id,
|
id,
|
||||||
ty,
|
ty,
|
||||||
relation,
|
relation,
|
||||||
column,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for GeormField {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
self.column
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| self.ident.to_string())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&GeormField> for proc_macro2::TokenStream {
|
impl From<&GeormField> for proc_macro2::TokenStream {
|
||||||
fn from(value: &GeormField) -> Self {
|
fn from(value: &GeormField) -> Self {
|
||||||
let Some(relation) = value.relation.clone() else {
|
let Some(relation) = value.relation.clone() else {
|
||||||
return quote! {};
|
return quote! {};
|
||||||
};
|
};
|
||||||
|
|
||||||
let function = syn::Ident::new(
|
let function = syn::Ident::new(
|
||||||
&format!("get_{}", relation.name),
|
&format!("get_{}", relation.name),
|
||||||
proc_macro2::Span::call_site(),
|
proc_macro2::Span::call_site(),
|
||||||
@ -225,8 +200,8 @@ impl From<&GeormField> for proc_macro2::TokenStream {
|
|||||||
quote! { fetch_one }
|
quote! { fetch_one }
|
||||||
};
|
};
|
||||||
quote! {
|
quote! {
|
||||||
pub async fn #function(&value, pool: &::sqlx::PgPool) -> ::sqlx::Result<#return_type> {
|
pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<#return_type> {
|
||||||
query_as!(#entity, #query, value.#local_ident).#fetch(pool).await
|
::sqlx::query_as!(#entity, #query, self.#local_ident).#fetch(pool).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,12 +49,11 @@ pub fn georm_derive_macro2(
|
|||||||
let struct_attrs: ir::GeormStructAttributes =
|
let struct_attrs: ir::GeormStructAttributes =
|
||||||
deluxe::extract_attributes(&mut ast).expect("Could not extract attributes from struct");
|
deluxe::extract_attributes(&mut ast).expect("Could not extract attributes from struct");
|
||||||
let (fields, id) = extract_georm_field_attrs(&mut ast)?;
|
let (fields, id) = extract_georm_field_attrs(&mut ast)?;
|
||||||
let trait_impl = trait_implementation::derive_trait(&ast, &struct_attrs.table, &fields, &id);
|
|
||||||
let relationships = relationships::derive_relationships(&ast, &struct_attrs, &fields, &id);
|
let relationships = relationships::derive_relationships(&ast, &struct_attrs, &fields, &id);
|
||||||
|
let trait_impl = trait_implementation::derive_trait(&ast, &struct_attrs.table, &fields, &id);
|
||||||
let code = quote! {
|
let code = quote! {
|
||||||
#trait_impl
|
|
||||||
#relationships
|
#relationships
|
||||||
|
#trait_impl
|
||||||
};
|
};
|
||||||
println!("{code}");
|
|
||||||
Ok(code)
|
Ok(code)
|
||||||
}
|
}
|
||||||
|
@ -35,12 +35,12 @@ pub fn derive_relationships(
|
|||||||
id: &GeormField,
|
id: &GeormField,
|
||||||
) -> TokenStream {
|
) -> TokenStream {
|
||||||
let struct_name = &ast.ident;
|
let struct_name = &ast.ident;
|
||||||
let one_to_one = derive(fields, |field| field.relation.is_none());
|
let one_to_one = derive(fields, |field| field.relation.is_some());
|
||||||
let one_to_many = derive(&struct_attrs.one_to_many, |_| true);
|
let one_to_many = derive(&struct_attrs.one_to_many, |_| true);
|
||||||
let many_to_many: Vec<M2MRelationshipComplete> = struct_attrs
|
let many_to_many: Vec<M2MRelationshipComplete> = struct_attrs
|
||||||
.many_to_many
|
.many_to_many
|
||||||
.iter()
|
.iter()
|
||||||
.map(|v| M2MRelationshipComplete::new(v, &struct_attrs.table, id.to_string()))
|
.map(|v| M2MRelationshipComplete::new(v, &struct_attrs.table, id.ident.to_string()))
|
||||||
.collect();
|
.collect();
|
||||||
let many_to_many = derive(&many_to_many, |_| true);
|
let many_to_many = derive(&many_to_many, |_| true);
|
||||||
|
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
use super::ir::GeormField;
|
use super::ir::GeormField;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
|
fn generate_find_all_query(table: &str) -> proc_macro2::TokenStream {
|
||||||
|
let find_string = format!("SELECT * FROM {table}");
|
||||||
|
quote! {
|
||||||
|
async fn find_all(pool: &::sqlx::PgPool) -> ::sqlx::Result<Vec<Self>> {
|
||||||
|
::sqlx::query_as!(Self, #find_string).fetch_all(pool).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn generate_find_query(table: &str, id: &GeormField) -> proc_macro2::TokenStream {
|
fn generate_find_query(table: &str, id: &GeormField) -> proc_macro2::TokenStream {
|
||||||
let find_string = format!("SELECT * FROM {table} WHERE {id} = $1",);
|
let find_string = format!("SELECT * FROM {table} WHERE {} = $1", id.ident);
|
||||||
let ty = &id.ty;
|
let ty = &id.ty;
|
||||||
quote! {
|
quote! {
|
||||||
async fn find(pool: &::sqlx::PgPool, id: &#ty) -> ::sqlx::Result<Option<Self>> {
|
async fn find(pool: &::sqlx::PgPool, id: &#ty) -> ::sqlx::Result<Option<Self>> {
|
||||||
@ -19,7 +28,7 @@ fn generate_create_query(table: &str, fields: &[GeormField]) -> proc_macro2::Tok
|
|||||||
"INSERT INTO {table} ({}) VALUES ({}) RETURNING *",
|
"INSERT INTO {table} ({}) VALUES ({}) RETURNING *",
|
||||||
fields
|
fields
|
||||||
.iter()
|
.iter()
|
||||||
.map(std::string::ToString::to_string)
|
.map(|f| f.ident.to_string())
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(", "),
|
.join(", "),
|
||||||
inputs.join(", ")
|
inputs.join(", ")
|
||||||
@ -47,11 +56,12 @@ fn generate_update_query(
|
|||||||
let update_columns = fields
|
let update_columns = fields
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, &field)| format!("{field} = ${}", i + 1))
|
.map(|(i, &field)| format!("{} = ${}", field.ident, i + 1))
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
let update_string = format!(
|
let update_string = format!(
|
||||||
"UPDATE {table} SET {update_columns} WHERE {id} = ${} RETURNING *",
|
"UPDATE {table} SET {update_columns} WHERE {} = ${} RETURNING *",
|
||||||
|
id.ident,
|
||||||
fields.len() + 1
|
fields.len() + 1
|
||||||
);
|
);
|
||||||
fields.push(id);
|
fields.push(id);
|
||||||
@ -70,7 +80,7 @@ fn generate_update_query(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn generate_delete_query(table: &str, id: &GeormField) -> proc_macro2::TokenStream {
|
fn generate_delete_query(table: &str, id: &GeormField) -> proc_macro2::TokenStream {
|
||||||
let delete_string = format!("DELETE FROM {table} WHERE {id} = $1");
|
let delete_string = format!("DELETE FROM {table} WHERE {} = $1", id.ident);
|
||||||
let ty = &id.ty;
|
let ty = &id.ty;
|
||||||
quote! {
|
quote! {
|
||||||
async fn delete_by_id(pool: &::sqlx::PgPool, id: &#ty) -> ::sqlx::Result<u64> {
|
async fn delete_by_id(pool: &::sqlx::PgPool, id: &#ty) -> ::sqlx::Result<u64> {
|
||||||
@ -104,13 +114,13 @@ pub fn derive_trait(
|
|||||||
id: &GeormField,
|
id: &GeormField,
|
||||||
) -> proc_macro2::TokenStream {
|
) -> proc_macro2::TokenStream {
|
||||||
let ty = &id.ty;
|
let ty = &id.ty;
|
||||||
let id_ident = &id.ident;
|
|
||||||
|
|
||||||
// define impl variables
|
// define impl variables
|
||||||
let ident = &ast.ident;
|
let ident = &ast.ident;
|
||||||
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
|
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
|
||||||
|
|
||||||
// generate
|
// generate
|
||||||
|
let get_all = generate_find_all_query(table);
|
||||||
let get_id = generate_get_id(id);
|
let get_id = generate_get_id(id);
|
||||||
let find_query = generate_find_query(table, id);
|
let find_query = generate_find_query(table, id);
|
||||||
let create_query = generate_create_query(table, fields);
|
let create_query = generate_create_query(table, fields);
|
||||||
@ -118,19 +128,11 @@ pub fn derive_trait(
|
|||||||
let delete_query = generate_delete_query(table, id);
|
let delete_query = generate_delete_query(table, id);
|
||||||
quote! {
|
quote! {
|
||||||
impl #impl_generics Georm<#ty> for #ident #type_generics #where_clause {
|
impl #impl_generics Georm<#ty> for #ident #type_generics #where_clause {
|
||||||
|
#get_all
|
||||||
#get_id
|
#get_id
|
||||||
#find_query
|
#find_query
|
||||||
#create_query
|
#create_query
|
||||||
#update_query
|
#update_query
|
||||||
|
|
||||||
async fn create_or_update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Self> {
|
|
||||||
if Self::find(pool, &self.#id_ident).await?.is_some() {
|
|
||||||
self.update(pool).await
|
|
||||||
} else {
|
|
||||||
self.create(pool).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#delete_query
|
#delete_query
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
33
justfile
33
justfile
@ -2,17 +2,11 @@ mod docker
|
|||||||
|
|
||||||
default: lint
|
default: lint
|
||||||
|
|
||||||
format:
|
clean:
|
||||||
cargo fmt --all
|
cargo clean
|
||||||
|
|
||||||
format-check:
|
test:
|
||||||
cargo fmt --check --all
|
cargo test --all-targets --all
|
||||||
|
|
||||||
build:
|
|
||||||
cargo build
|
|
||||||
|
|
||||||
build-release:
|
|
||||||
cargo build --release
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
cargo clippy --all-targets
|
cargo clippy --all-targets
|
||||||
@ -20,18 +14,19 @@ lint:
|
|||||||
audit:
|
audit:
|
||||||
cargo deny check all
|
cargo deny check all
|
||||||
|
|
||||||
test:
|
build:
|
||||||
cargo test --all-targets --all
|
cargo build
|
||||||
|
|
||||||
coverage:
|
build-release:
|
||||||
mkdir -p coverage
|
cargo build --release
|
||||||
cargo tarpaulin --config .tarpaulin.local.toml
|
|
||||||
|
|
||||||
coverage-ci:
|
format:
|
||||||
mkdir -p coverage
|
cargo fmt --all
|
||||||
cargo tarpaulin --config .tarpaulin.ci.toml
|
|
||||||
|
|
||||||
check-all: format-check lint coverage audit
|
format-check:
|
||||||
|
cargo fmt --check --all
|
||||||
|
|
||||||
|
check-all: format-check lint audit test
|
||||||
|
|
||||||
## Local Variables:
|
## Local Variables:
|
||||||
## mode: makefile
|
## mode: makefile
|
||||||
|
6
migrations/20250126153330_simple-struct-tests.down.sql
Normal file
6
migrations/20250126153330_simple-struct-tests.down.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
DROP TABLE IF EXISTS reviews;
|
||||||
|
DROP TABLE IF EXISTS book_genres;
|
||||||
|
DROP TABLE IF EXISTS books;
|
||||||
|
DROP TABLE IF EXISTS genres;
|
||||||
|
DROP TABLE IF EXISTS authors;
|
||||||
|
DROP TABLE IF EXISTS biographies;
|
38
migrations/20250126153330_simple-struct-tests.up.sql
Normal file
38
migrations/20250126153330_simple-struct-tests.up.sql
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
CREATE TABLE biographies (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
content TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE authors (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
biography_id INT,
|
||||||
|
FOREIGN KEY (biography_id) REFERENCES biographies(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE books (
|
||||||
|
ident SERIAL PRIMARY KEY,
|
||||||
|
title VARCHAR(100) NOT NULL,
|
||||||
|
author_id INT NOT NULL,
|
||||||
|
FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE reviews (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
book_id INT NOT NULL,
|
||||||
|
review TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (book_id) REFERENCES books(ident) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE genres (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE book_genres (
|
||||||
|
book_id INT NOT NULL,
|
||||||
|
genre_id INT NOT NULL,
|
||||||
|
PRIMARY KEY (book_id, genre_id),
|
||||||
|
FOREIGN KEY (book_id) REFERENCES books(ident) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (genre_id) REFERENCES genres(id) ON DELETE CASCADE
|
||||||
|
);
|
23
src/lib.rs
23
src/lib.rs
@ -1,6 +1,16 @@
|
|||||||
pub use georm_macros::Georm;
|
pub use georm_macros::Georm;
|
||||||
|
|
||||||
pub trait Georm<Id> {
|
pub trait Georm<Id> {
|
||||||
|
/// Find all the entities in the database.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns any error Postgres may have encountered
|
||||||
|
fn find_all(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
) -> impl ::std::future::Future<Output = ::sqlx::Result<Vec<Self>>> + Send
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
|
||||||
/// Find the entiy in the database based on its identifier.
|
/// Find the entiy in the database based on its identifier.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
@ -42,9 +52,18 @@ pub trait Georm<Id> {
|
|||||||
fn create_or_update(
|
fn create_or_update(
|
||||||
&self,
|
&self,
|
||||||
pool: &sqlx::PgPool,
|
pool: &sqlx::PgPool,
|
||||||
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
|
) -> impl ::std::future::Future<Output = sqlx::Result<Self>>
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized,
|
||||||
|
{
|
||||||
|
async {
|
||||||
|
if Self::find(pool, self.get_id()).await?.is_some() {
|
||||||
|
self.update(pool).await
|
||||||
|
} else {
|
||||||
|
self.create(pool).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete the entity from the database if it exists.
|
/// Delete the entity from the database if it exists.
|
||||||
///
|
///
|
||||||
|
10
tests/fixtures/o2o.sql
vendored
Normal file
10
tests/fixtures/o2o.sql
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
INSERT INTO books (title, author_id)
|
||||||
|
VALUES ('The Lord of the Rings: The Fellowship of the Ring', 1),
|
||||||
|
('The Lord of the Rings: The Two Towers', 1),
|
||||||
|
('The Lord of the Rings: The Return of the King', 1),
|
||||||
|
('To Build a Fire', 3);
|
||||||
|
|
||||||
|
INSERT INTO reviews (book_id, review)
|
||||||
|
VALUES (1, 'Great book'),
|
||||||
|
(3, 'Awesome book'),
|
||||||
|
(2, 'Greatest book');
|
8
tests/fixtures/simple_struct.sql
vendored
Normal file
8
tests/fixtures/simple_struct.sql
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
INSERT INTO biographies (content)
|
||||||
|
VALUES ('Some text'),
|
||||||
|
('Some other text');
|
||||||
|
|
||||||
|
INSERT INTO authors (name, biography_id)
|
||||||
|
VALUES ('J.R.R. Tolkien', 2),
|
||||||
|
('George Orwell', NULL),
|
||||||
|
('Jack London', 1);
|
63
tests/models.rs
Normal file
63
tests/models.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use georm::Georm;
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow, Georm, PartialEq, Eq, Default)]
|
||||||
|
#[georm(table = "biographies")]
|
||||||
|
pub struct Biography {
|
||||||
|
#[georm(id)]
|
||||||
|
pub id: i32,
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow, Georm, PartialEq, Eq, Default)]
|
||||||
|
#[georm(table = "authors")]
|
||||||
|
pub struct Author {
|
||||||
|
#[georm(id)]
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
#[georm(relation = {entity = Biography, table = "biographies", name = "biography", nullable = true})]
|
||||||
|
pub biography_id: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Author {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.id.cmp(&other.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Author {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.id.cmp(&other.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow, Georm, PartialEq, Eq, Default)]
|
||||||
|
#[georm(table = "books")]
|
||||||
|
pub struct Book {
|
||||||
|
#[georm(id)]
|
||||||
|
ident: i32,
|
||||||
|
title: String,
|
||||||
|
#[georm(relation = {entity = Author, table = "authors", name = "author"})]
|
||||||
|
author_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Book {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.ident.cmp(&other.ident))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Book {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.ident.cmp(&other.ident)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow, Georm, PartialEq, Eq)]
|
||||||
|
#[georm(table = "reviews")]
|
||||||
|
pub struct Review {
|
||||||
|
#[georm(id)]
|
||||||
|
pub id: i32,
|
||||||
|
#[georm(relation = {entity = Book, table = "books", remote_id = "ident", name = "book"})]
|
||||||
|
pub book_id: i32,
|
||||||
|
pub review: String
|
||||||
|
}
|
55
tests/o2o_relationship.rs
Normal file
55
tests/o2o_relationship.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
use georm::Georm;
|
||||||
|
|
||||||
|
mod models;
|
||||||
|
use models::*;
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("simple_struct", "o2o"))]
|
||||||
|
async fn book_should_have_working_get_author_method(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let book = Book::find(&pool, &1).await?;
|
||||||
|
assert!(book.is_some());
|
||||||
|
let book = book.unwrap();
|
||||||
|
let author = book.get_author(&pool).await?;
|
||||||
|
let expected_author = Author {
|
||||||
|
id: 1,
|
||||||
|
name: "J.R.R. Tolkien".into(),
|
||||||
|
biography_id: Some(2),
|
||||||
|
};
|
||||||
|
assert_eq!(expected_author, author);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("simple_struct"))]
|
||||||
|
async fn author_should_have_working_get_biography_method(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let author = Author::find(&pool, &1).await?;
|
||||||
|
assert!(author.is_some());
|
||||||
|
let author = author.unwrap();
|
||||||
|
let biography = author.get_biography(&pool).await?;
|
||||||
|
assert!(biography.is_some());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("simple_struct"))]
|
||||||
|
async fn author_should_have_optional_biographies(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let tolkien = Author::find(&pool, &1).await?;
|
||||||
|
assert!(tolkien.is_some());
|
||||||
|
let tolkien_biography = tolkien.unwrap().get_biography(&pool).await?;
|
||||||
|
assert!(tolkien_biography.is_some());
|
||||||
|
let biography = Biography {
|
||||||
|
id: 2,
|
||||||
|
content: "Some other text".into(),
|
||||||
|
};
|
||||||
|
assert_eq!(biography, tolkien_biography.unwrap());
|
||||||
|
let orwell = Author::find(&pool, &2).await?;
|
||||||
|
assert!(orwell.is_some());
|
||||||
|
assert!(orwell.unwrap().get_biography(&pool).await?.is_none());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("simple_struct", "o2o"))]
|
||||||
|
async fn books_are_found_despite_nonstandard_id_name(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let review = Review::find(&pool, &1).await?.unwrap();
|
||||||
|
let book = review.get_book(&pool).await?;
|
||||||
|
let tolkien = Author::find(&pool, &1).await?.unwrap();
|
||||||
|
assert_eq!(tolkien, book.get_author(&pool).await?);
|
||||||
|
Ok(())
|
||||||
|
}
|
164
tests/simple_struct.rs
Normal file
164
tests/simple_struct.rs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
use georm::Georm;
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
|
||||||
|
use models::Author;
|
||||||
|
mod models;
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("simple_struct"))]
|
||||||
|
async fn find_all_query_works(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let result = Author::find_all(&pool).await?;
|
||||||
|
assert_eq!(3, result.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn find_all_returns_empty_vec_on_empty_table(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let result = Author::find_all(&pool).await?;
|
||||||
|
assert_eq!(0, result.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("simple_struct"))]
|
||||||
|
async fn find_query_works(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let id = 1;
|
||||||
|
let res = Author::find(&pool, &id).await?;
|
||||||
|
assert!(res.is_some());
|
||||||
|
let res = res.unwrap();
|
||||||
|
assert_eq!(String::from("J.R.R. Tolkien"), res.name);
|
||||||
|
assert_eq!(1, res.id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn find_returns_none_if_not_found(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let res = Author::find(&pool, &420).await?;
|
||||||
|
assert!(res.is_none());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn create_works(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let author = Author {
|
||||||
|
id: 1,
|
||||||
|
name: "J.R.R. Tolkien".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
author.create(&pool).await?;
|
||||||
|
let all_authors = Author::find_all(&pool).await?;
|
||||||
|
assert_eq!(1, all_authors.len());
|
||||||
|
assert_eq!(vec![author], all_authors);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("simple_struct"))]
|
||||||
|
async fn create_fails_if_already_exists(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let author = Author {
|
||||||
|
id: 2,
|
||||||
|
name: "Miura Kentaro".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let result = author.create(&pool).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
let error = result.err().unwrap();
|
||||||
|
assert_eq!("error returned from database: duplicate key value violates unique constraint \"authors_pkey\"", error.to_string());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("simple_struct"))]
|
||||||
|
async fn update_works(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let expected_initial = Author {
|
||||||
|
name: "J.R.R. Tolkien".into(),
|
||||||
|
id: 1,
|
||||||
|
biography_id: Some(2),
|
||||||
|
};
|
||||||
|
let expected_final = Author {
|
||||||
|
name: "Jolkien Rolkien Rolkien Tolkien".into(),
|
||||||
|
id: 1,
|
||||||
|
biography_id: Some(2),
|
||||||
|
};
|
||||||
|
let tolkien = Author::find(&pool, &1).await?;
|
||||||
|
assert!(tolkien.is_some());
|
||||||
|
let mut tolkien = tolkien.unwrap();
|
||||||
|
assert_eq!(expected_initial, tolkien);
|
||||||
|
tolkien.name = expected_final.name.clone();
|
||||||
|
let updated = tolkien.update(&pool).await?;
|
||||||
|
assert_eq!(expected_final, updated);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn update_fails_if_not_already_exists(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let author = Author {
|
||||||
|
id: 2,
|
||||||
|
name: "Miura Kentaro".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let result = author.update(&pool).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
let error = result.err().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
"no rows returned by a query that expected to return at least one row",
|
||||||
|
error.to_string()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
async fn should_create_if_does_not_exist(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let all_authors = Author::find_all(&pool).await?;
|
||||||
|
assert_eq!(0, all_authors.len());
|
||||||
|
let author = Author {
|
||||||
|
id: 4,
|
||||||
|
name: "Miura Kentaro".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
author.create_or_update(&pool).await?;
|
||||||
|
let all_authors = Author::find_all(&pool).await?;
|
||||||
|
assert_eq!(1, all_authors.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("simple_struct"))]
|
||||||
|
async fn should_update_if_exist(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let all_authors = Author::find_all(&pool).await?;
|
||||||
|
assert_eq!(3, all_authors.len());
|
||||||
|
let author = Author {
|
||||||
|
id: 2,
|
||||||
|
name: "Miura Kentaro".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
author.create_or_update(&pool).await?;
|
||||||
|
let mut all_authors = Author::find_all(&pool).await?;
|
||||||
|
all_authors.sort();
|
||||||
|
assert_eq!(3, all_authors.len());
|
||||||
|
assert_eq!(author, all_authors[1]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("simple_struct"))]
|
||||||
|
async fn delete_by_id_should_delete_only_one_entry(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let id = 2;
|
||||||
|
let all_authors = Author::find_all(&pool).await?;
|
||||||
|
assert_eq!(3, all_authors.len());
|
||||||
|
assert!(all_authors.iter().any(|author| author.get_id() == &id));
|
||||||
|
let result = Author::delete_by_id(&pool, &id).await?;
|
||||||
|
assert_eq!(1, result);
|
||||||
|
let all_authors = Author::find_all(&pool).await?;
|
||||||
|
assert_eq!(2, all_authors.len());
|
||||||
|
assert!(all_authors.iter().all(|author| author.get_id() != &id));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("simple_struct"))]
|
||||||
|
async fn delete_should_delete_current_entity_from_db(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
||||||
|
let mut all_authors = Author::find_all(&pool).await?;
|
||||||
|
assert_eq!(3, all_authors.len());
|
||||||
|
all_authors.shuffle(&mut rand::rng());
|
||||||
|
let author = all_authors.first().unwrap();
|
||||||
|
let result = author.delete(&pool).await?;
|
||||||
|
assert_eq!(1, result);
|
||||||
|
let all_authors = Author::find_all(&pool).await?;
|
||||||
|
assert_eq!(2, all_authors.len());
|
||||||
|
assert!(all_authors.iter().all(|a| a.get_id() != author.get_id()));
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user