diff --git a/.tarpaulin.ci.toml b/.tarpaulin.ci.toml deleted file mode 100644 index 17a1b5c..0000000 --- a/.tarpaulin.ci.toml +++ /dev/null @@ -1,7 +0,0 @@ -[all] -out = ["Xml"] -target-dir = "coverage" -output-dir = "coverage" -fail-under = 40 -exclude-files = ["target/*"] -run-types = ["AllTargets"] diff --git a/.tarpaulin.local.toml b/.tarpaulin.local.toml deleted file mode 100644 index f1227c1..0000000 --- a/.tarpaulin.local.toml +++ /dev/null @@ -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"] \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 207674c..cf64eb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -418,6 +418,7 @@ name = "georm" version = "0.1.0" dependencies = [ "georm-macros", + "rand 0.9.0", "sqlx", ] @@ -439,7 +440,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "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]] @@ -765,7 +778,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -781,7 +794,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -920,7 +933,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -958,8 +971,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "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]] @@ -969,7 +993,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "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]] @@ -978,7 +1012,17 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 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]] @@ -1003,7 +1047,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -1114,7 +1158,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1276,7 +1320,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.5", "rsa", "sha1", "sha2", @@ -1313,7 +1357,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand", + "rand 0.8.5", "serde", "serde_json", "sha2", @@ -1406,7 +1450,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", - "getrandom", + "getrandom 0.2.15", "once_cell", "rustix", "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" 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]] name = "wasite" version = "0.1.0" @@ -1779,6 +1832,15 @@ dependencies = [ "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]] name = "write16" version = "1.0.0" @@ -1822,7 +1884,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "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]] @@ -1836,6 +1907,17 @@ dependencies = [ "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]] name = "zerofrom" version = "0.1.5" diff --git a/Cargo.toml b/Cargo.toml index 6d95de8..b3393ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,9 @@ features = ["postgres", "runtime-tokio", "macros", "migrate"] sqlx = { workspace = true } georm-macros = { workspace = true } +[dev-dependencies] +rand = "0.9" + [workspace.lints.rust] unsafe_code = "forbid" diff --git a/deny.toml b/deny.toml index 8a8b70e..399e8a7 100644 --- a/deny.toml +++ b/deny.toml @@ -1,11 +1,12 @@ [licenses] -# If there is a need to add another license, please refer to this -# page: https://www.gnu.org/licenses/license-list.html +# If there is a need to add another license, please refer to this page +# for compatible licenses: +# https://www.gnu.org/licenses/license-list.html allow = ["MIT", "Apache-2.0", "BSD-3-Clause", "Unicode-3.0", "Zlib"] confidence-threshold = 0.8 [bans] -multiple-versions = "warn" +multiple-versions = "allow" wildcards = "allow" highlight = "all" workspace-default-features = "allow" diff --git a/flake.nix b/flake.nix index 0282d2c..e431b32 100644 --- a/flake.nix +++ b/flake.nix @@ -39,7 +39,6 @@ SQLX_OFFLINE="1" cargo build --release bacon cargo cargo-deny - cargo-tarpaulin just rust-analyzer (rustVersion.override { diff --git a/georm-macros/src/georm/ir.rs b/georm-macros/src/georm/ir.rs index b447a86..2090339 100644 --- a/georm-macros/src/georm/ir.rs +++ b/georm-macros/src/georm/ir.rs @@ -1,5 +1,4 @@ use quote::quote; -use std::fmt::{self, Display}; #[derive(deluxe::ExtractAttributes)] #[deluxe(attributes(georm))] @@ -32,7 +31,7 @@ impl From<&O2MRelationship> for proc_macro2::TokenStream { ); quote! { pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result> { - 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! { pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result> { - 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)] pub id: bool, #[deluxe(default = None)] - pub column: Option, - #[deluxe(default = None)] pub relation: Option, } -// #[georm( -// table = "profileId", -// one_to_one = { name = profile, id = "id", entity = Profile, nullable } -// )] -#[derive(deluxe::ParseMetaItem, Clone)] +// #[georm(relation = { name = profile, id = "id", entity = Profile, nullable })] +#[derive(deluxe::ParseMetaItem, Clone, Debug)] pub struct O2ORelationship { pub entity: syn::Type, pub table: String, @@ -154,12 +148,11 @@ pub struct O2ORelationship { pub name: String, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct GeormField { pub ident: syn::Ident, pub field: syn::Field, pub ty: syn::Type, - pub column: Option, pub id: bool, pub relation: Option, } @@ -170,40 +163,22 @@ impl GeormField { let ty = field.clone().ty; let attrs: GeormFieldAttributes = deluxe::extract_attributes(field).expect("Could not extract attributes from field"); - let GeormFieldAttributes { - id, - column, - relation, - } = attrs; + let GeormFieldAttributes { id, relation } = attrs; Self { ident, field: field.to_owned(), id, ty, 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 { fn from(value: &GeormField) -> Self { let Some(relation) = value.relation.clone() else { return quote! {}; }; - let function = syn::Ident::new( &format!("get_{}", relation.name), proc_macro2::Span::call_site(), @@ -225,8 +200,8 @@ impl From<&GeormField> for proc_macro2::TokenStream { quote! { fetch_one } }; quote! { - pub async fn #function(&value, pool: &::sqlx::PgPool) -> ::sqlx::Result<#return_type> { - query_as!(#entity, #query, value.#local_ident).#fetch(pool).await + pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<#return_type> { + ::sqlx::query_as!(#entity, #query, self.#local_ident).#fetch(pool).await } } } diff --git a/georm-macros/src/georm/mod.rs b/georm-macros/src/georm/mod.rs index f8e39c7..32f3570 100644 --- a/georm-macros/src/georm/mod.rs +++ b/georm-macros/src/georm/mod.rs @@ -49,12 +49,11 @@ pub fn georm_derive_macro2( let struct_attrs: ir::GeormStructAttributes = deluxe::extract_attributes(&mut ast).expect("Could not extract attributes from struct"); 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 trait_impl = trait_implementation::derive_trait(&ast, &struct_attrs.table, &fields, &id); let code = quote! { - #trait_impl #relationships + #trait_impl }; - println!("{code}"); Ok(code) } diff --git a/georm-macros/src/georm/relationships.rs b/georm-macros/src/georm/relationships.rs index 3f9e2a3..f51c47b 100644 --- a/georm-macros/src/georm/relationships.rs +++ b/georm-macros/src/georm/relationships.rs @@ -35,12 +35,12 @@ pub fn derive_relationships( id: &GeormField, ) -> TokenStream { 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 many_to_many: Vec = struct_attrs .many_to_many .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(); let many_to_many = derive(&many_to_many, |_| true); diff --git a/georm-macros/src/georm/trait_implementation.rs b/georm-macros/src/georm/trait_implementation.rs index 08dc57e..6da75c7 100644 --- a/georm-macros/src/georm/trait_implementation.rs +++ b/georm-macros/src/georm/trait_implementation.rs @@ -1,8 +1,17 @@ use super::ir::GeormField; 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> { + ::sqlx::query_as!(Self, #find_string).fetch_all(pool).await + } + } +} + 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; quote! { async fn find(pool: &::sqlx::PgPool, id: &#ty) -> ::sqlx::Result> { @@ -19,7 +28,7 @@ fn generate_create_query(table: &str, fields: &[GeormField]) -> proc_macro2::Tok "INSERT INTO {table} ({}) VALUES ({}) RETURNING *", fields .iter() - .map(std::string::ToString::to_string) + .map(|f| f.ident.to_string()) .collect::>() .join(", "), inputs.join(", ") @@ -47,11 +56,12 @@ fn generate_update_query( let update_columns = fields .iter() .enumerate() - .map(|(i, &field)| format!("{field} = ${}", i + 1)) + .map(|(i, &field)| format!("{} = ${}", field.ident, i + 1)) .collect::>() .join(", "); 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.push(id); @@ -70,7 +80,7 @@ fn generate_update_query( } 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; quote! { async fn delete_by_id(pool: &::sqlx::PgPool, id: &#ty) -> ::sqlx::Result { @@ -104,13 +114,13 @@ pub fn derive_trait( id: &GeormField, ) -> proc_macro2::TokenStream { let ty = &id.ty; - let id_ident = &id.ident; // define impl variables let ident = &ast.ident; let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); // generate + let get_all = generate_find_all_query(table); let get_id = generate_get_id(id); let find_query = generate_find_query(table, id); let create_query = generate_create_query(table, fields); @@ -118,19 +128,11 @@ pub fn derive_trait( let delete_query = generate_delete_query(table, id); quote! { impl #impl_generics Georm<#ty> for #ident #type_generics #where_clause { + #get_all #get_id #find_query #create_query #update_query - - async fn create_or_update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { - if Self::find(pool, &self.#id_ident).await?.is_some() { - self.update(pool).await - } else { - self.create(pool).await - } - } - #delete_query } } diff --git a/justfile b/justfile index 6ae10ca..30633cb 100644 --- a/justfile +++ b/justfile @@ -2,17 +2,11 @@ mod docker default: lint -format: - cargo fmt --all +clean: + cargo clean -format-check: - cargo fmt --check --all - -build: - cargo build - -build-release: - cargo build --release +test: + cargo test --all-targets --all lint: cargo clippy --all-targets @@ -20,18 +14,19 @@ lint: audit: cargo deny check all -test: - cargo test --all-targets --all +build: + cargo build -coverage: - mkdir -p coverage - cargo tarpaulin --config .tarpaulin.local.toml +build-release: + cargo build --release -coverage-ci: - mkdir -p coverage - cargo tarpaulin --config .tarpaulin.ci.toml +format: + cargo fmt --all -check-all: format-check lint coverage audit +format-check: + cargo fmt --check --all + +check-all: format-check lint audit test ## Local Variables: ## mode: makefile diff --git a/migrations/20250126153330_simple-struct-tests.down.sql b/migrations/20250126153330_simple-struct-tests.down.sql new file mode 100644 index 0000000..ad6a6e3 --- /dev/null +++ b/migrations/20250126153330_simple-struct-tests.down.sql @@ -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; diff --git a/migrations/20250126153330_simple-struct-tests.up.sql b/migrations/20250126153330_simple-struct-tests.up.sql new file mode 100644 index 0000000..389a32a --- /dev/null +++ b/migrations/20250126153330_simple-struct-tests.up.sql @@ -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 +); diff --git a/src/lib.rs b/src/lib.rs index cfd72b0..64dffd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,16 @@ pub use georm_macros::Georm; pub trait Georm { + /// 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>> + Send + where + Self: Sized; + /// Find the entiy in the database based on its identifier. /// /// # Errors @@ -42,9 +52,18 @@ pub trait Georm { fn create_or_update( &self, pool: &sqlx::PgPool, - ) -> impl std::future::Future> + Send + ) -> impl ::std::future::Future> 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. /// diff --git a/tests/fixtures/o2o.sql b/tests/fixtures/o2o.sql new file mode 100644 index 0000000..9ac8534 --- /dev/null +++ b/tests/fixtures/o2o.sql @@ -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'); diff --git a/tests/fixtures/simple_struct.sql b/tests/fixtures/simple_struct.sql new file mode 100644 index 0000000..e280e38 --- /dev/null +++ b/tests/fixtures/simple_struct.sql @@ -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); diff --git a/tests/models.rs b/tests/models.rs new file mode 100644 index 0000000..bef3224 --- /dev/null +++ b/tests/models.rs @@ -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, +} + +impl PartialOrd for Author { + fn partial_cmp(&self, other: &Self) -> Option { + 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 { + 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 +} diff --git a/tests/o2o_relationship.rs b/tests/o2o_relationship.rs new file mode 100644 index 0000000..ba74c05 --- /dev/null +++ b/tests/o2o_relationship.rs @@ -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(()) +} diff --git a/tests/simple_struct.rs b/tests/simple_struct.rs new file mode 100644 index 0000000..182eb94 --- /dev/null +++ b/tests/simple_struct.rs @@ -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(()) +}