diff --git a/Cargo.lock b/Cargo.lock index cf64eb3..205286c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,9 +73,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" dependencies = [ "serde", ] @@ -97,9 +97,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "cfg-if" @@ -254,18 +254,18 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" dependencies = [ "serde", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" @@ -415,7 +415,7 @@ dependencies = [ [[package]] name = "georm" -version = "0.1.0" +version = "0.1.1" dependencies = [ "georm-macros", "rand 0.9.0", @@ -424,7 +424,7 @@ dependencies = [ [[package]] name = "georm-macros" -version = "0.1.0" +version = "0.1.1" dependencies = [ "deluxe", "proc-macro2", @@ -698,9 +698,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.169" +version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "libm" @@ -726,9 +726,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "litemap" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "lock_api" @@ -742,9 +742,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.25" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "md-5" @@ -764,9 +764,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", ] @@ -840,9 +840,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "parking" @@ -982,8 +982,8 @@ 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", + "rand_core 0.9.3", + "zerocopy 0.8.21", ] [[package]] @@ -1003,7 +1003,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -1017,19 +1017,18 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.1", - "zerocopy 0.8.14", ] [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" dependencies = [ "bitflags", ] @@ -1075,9 +1074,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "scopeguard" @@ -1087,18 +1086,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" dependencies = [ "proc-macro2", "quote", @@ -1107,9 +1106,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.137" +version = "1.0.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" +checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" dependencies = [ "itoa", "memchr", @@ -1172,9 +1171,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" dependencies = [ "serde", ] @@ -1422,9 +1421,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.96" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -1444,13 +1443,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.15.0" +version = "3.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" dependencies = [ "cfg-if", "fastrand", - "getrandom 0.2.15", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", @@ -1578,9 +1577,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicode-bidi" @@ -1590,9 +1589,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243" +checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" [[package]] name = "unicode-normalization" @@ -1889,11 +1888,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.14" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +checksum = "dcf01143b2dd5d134f11f545cf9f1431b13b749695cb33bcce051e7568f99478" dependencies = [ - "zerocopy-derive 0.8.14", + "zerocopy-derive 0.8.21", ] [[package]] @@ -1909,9 +1908,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.8.14" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +checksum = "712c8386f4f4299382c9abee219bee7084f78fb939d88b6840fcc1320d5f6da2" dependencies = [ "proc-macro2", "quote", @@ -1920,18 +1919,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", diff --git a/README.md b/README.md index 9b0159a..c4c3ba3 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,16 @@ +
+ + Georm logo + +
+
+

Georm

A simple, opinionated SQLx ORM for PostgreSQL
-
@@ -13,26 +19,22 @@ actions status - Crates.io version - + Crates.io version + - docs.rs docs + docs.rs docs +
-
-

What is Georm?

-
+## What is Georm? Georm is a quite simple ORM built around [SQLx](https://crates.io/crates/sqlx) that gives access to a few useful functions when interacting with a database, implementing automatically the most basic SQL interactions you’re tired of writing. -
-

Why is Georm?

-
+## Why is Georm? I wanted an ORM that’s easy and straightforward to use. I am aware some other projects exist, such as @@ -40,16 +42,12 @@ some other projects exist, such as my needs and/or my wants of a simple interface. I ended up writing the ORM I wanted to use. -
-

How is Georm?

-
+## How is Georm? I use it in a few projects, and I’m quite happy with it right now. But of course, I’m open to constructive criticism and suggestions! -
-

How can I use it?

-
+## How can I use it? Georm works with SQLx, but does not re-export it itself. To get started, install both Georm and SQLx in your Rust project: @@ -121,9 +119,7 @@ pub struct Author { Congratulations, your struct `Author` now has access to all the functions described in the `Georm` trait! -
-

Entity relationship

-
+## Entity relationship It is possible to implement one-to-one, one-to-many, and many-to-many relationships with Georm. This is a quick example of how a struct with @@ -132,8 +128,12 @@ several relationships of different types may be declared: #[derive(sqlx::FromRow, Georm)] #[georm( table = "books", + one_to_one = [ + { name = "draft", remote_id = "book_id", table = "drafts", entity = Draft } + ], one_to_many = [ - { name = "reviews", remote_id = "book_id", table = "reviews", entity = Review } + { name = "reviews", remote_id = "book_id", table = "reviews", entity = Review }, + { name = "reprints", remote_id = "book_id", table = "reprints", entity = Reprint } ], many_to_many = [{ name = "genres", diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..8a4239f Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100644 index 0000000..61b5e0a --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1,1272 @@ + + + + diff --git a/georm-macros/src/georm/ir.rs b/georm-macros/src/georm/ir.rs deleted file mode 100644 index 9045db2..0000000 --- a/georm-macros/src/georm/ir.rs +++ /dev/null @@ -1,195 +0,0 @@ -use quote::quote; - -#[derive(deluxe::ExtractAttributes)] -#[deluxe(attributes(georm))] -pub struct GeormStructAttributes { - pub table: String, - #[deluxe(default = Vec::new())] - pub one_to_many: Vec, - #[deluxe(default = Vec::new())] - pub many_to_many: Vec, -} - -#[derive(deluxe::ParseMetaItem)] -pub struct O2MRelationship { - pub name: String, - pub remote_id: String, - pub table: String, - pub entity: syn::Type, -} - -impl From<&O2MRelationship> for proc_macro2::TokenStream { - fn from(value: &O2MRelationship) -> Self { - let query = format!( - "SELECT * FROM {} WHERE {} = $1", - value.table, value.remote_id - ); - let entity = &value.entity; - let function = syn::Ident::new( - &format!("get_{}", value.name), - proc_macro2::Span::call_site(), - ); - quote! { - pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result> { - ::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await - } - } - } -} - -#[derive(deluxe::ParseMetaItem, Clone)] -pub struct M2MLink { - pub table: String, - pub from: String, - pub to: String, -} - -#[derive(deluxe::ParseMetaItem)] -pub struct M2MRelationship { - pub name: String, - pub entity: syn::Type, - pub table: String, - #[deluxe(default = String::from("id"))] - pub remote_id: String, - pub link: M2MLink, -} - -pub struct Identifier { - pub table: String, - pub id: String, -} - -pub struct M2MRelationshipComplete { - pub name: String, - pub entity: syn::Type, - pub local: Identifier, - pub remote: Identifier, - pub link: M2MLink, -} - -impl M2MRelationshipComplete { - pub fn new(other: &M2MRelationship, local_table: &String, local_id: String) -> Self { - Self { - name: other.name.clone(), - entity: other.entity.clone(), - link: other.link.clone(), - local: Identifier { - table: local_table.to_string(), - id: local_id, - }, - remote: Identifier { - table: other.table.clone(), - id: other.remote_id.clone(), - }, - } - } -} - -impl From<&M2MRelationshipComplete> for proc_macro2::TokenStream { - fn from(value: &M2MRelationshipComplete) -> Self { - let function = syn::Ident::new( - &format!("get_{}", value.name), - proc_macro2::Span::call_site(), - ); - let entity = &value.entity; - let query = format!( - "SELECT remote.* -FROM {} local -JOIN {} link ON link.{} = local.{} -JOIN {} remote ON link.{} = remote.{} -WHERE local.{} = $1", - value.local.table, - value.link.table, - value.link.from, - value.local.id, - value.remote.table, - value.link.to, - value.remote.id, - value.local.id - ); - quote! { - pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result> { - ::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await - } - } - } -} - -#[derive(deluxe::ExtractAttributes, Clone)] -#[deluxe(attributes(georm))] -struct GeormFieldAttributes { - #[deluxe(default = false)] - pub id: bool, - #[deluxe(default = None)] - pub relation: Option, -} - -#[derive(deluxe::ParseMetaItem, Clone, Debug)] -pub struct O2ORelationship { - pub entity: syn::Type, - pub table: String, - #[deluxe(default = String::from("id"))] - pub remote_id: String, - #[deluxe(default = false)] - pub nullable: bool, - pub name: String, -} - -#[derive(Clone, Debug)] -pub struct GeormField { - pub ident: syn::Ident, - pub field: syn::Field, - pub ty: syn::Type, - pub id: bool, - pub relation: Option, -} - -impl GeormField { - pub fn new(field: &mut syn::Field) -> Self { - let ident = field.clone().ident.unwrap(); - let ty = field.clone().ty; - let attrs: GeormFieldAttributes = - deluxe::extract_attributes(field).expect("Could not extract attributes from field"); - let GeormFieldAttributes { id, relation } = attrs; - Self { - ident, - field: field.to_owned(), - id, - ty, - relation, - } - } -} - -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(), - ); - let entity = &relation.entity; - let return_type = if relation.nullable { - quote! { Option<#entity> } - } else { - quote! { #entity } - }; - let query = format!( - "SELECT * FROM {} WHERE {} = $1", - relation.table, relation.remote_id - ); - let local_ident = &value.field.ident; - let fetch = if relation.nullable { - quote! { fetch_optional } - } else { - quote! { fetch_one } - }; - quote! { - 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/ir/m2m_relationship.rs b/georm-macros/src/georm/ir/m2m_relationship.rs new file mode 100644 index 0000000..36091d5 --- /dev/null +++ b/georm-macros/src/georm/ir/m2m_relationship.rs @@ -0,0 +1,79 @@ +use quote::quote; + +#[derive(deluxe::ParseMetaItem, Clone)] +pub struct M2MLink { + pub table: String, + pub from: String, + pub to: String, +} + +#[derive(deluxe::ParseMetaItem)] +pub struct M2MRelationship { + pub name: String, + pub entity: syn::Type, + pub table: String, + #[deluxe(default = String::from("id"))] + pub remote_id: String, + pub link: M2MLink, +} + +pub struct Identifier { + pub table: String, + pub id: String, +} + +pub struct M2MRelationshipComplete { + pub name: String, + pub entity: syn::Type, + pub local: Identifier, + pub remote: Identifier, + pub link: M2MLink, +} + +impl M2MRelationshipComplete { + pub fn new(other: &M2MRelationship, local_table: &String, local_id: String) -> Self { + Self { + name: other.name.clone(), + entity: other.entity.clone(), + link: other.link.clone(), + local: Identifier { + table: local_table.to_string(), + id: local_id, + }, + remote: Identifier { + table: other.table.clone(), + id: other.remote_id.clone(), + }, + } + } +} + +impl From<&M2MRelationshipComplete> for proc_macro2::TokenStream { + fn from(value: &M2MRelationshipComplete) -> Self { + let function = syn::Ident::new( + &format!("get_{}", value.name), + proc_macro2::Span::call_site(), + ); + let entity = &value.entity; + let query = format!( + "SELECT remote.* +FROM {} local +JOIN {} link ON link.{} = local.{} +JOIN {} remote ON link.{} = remote.{} +WHERE local.{} = $1", + value.local.table, + value.link.table, + value.link.from, + value.local.id, + value.remote.table, + value.link.to, + value.remote.id, + value.local.id + ); + quote! { + pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result> { + ::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await + } + } + } +} diff --git a/georm-macros/src/georm/ir/mod.rs b/georm-macros/src/georm/ir/mod.rs new file mode 100644 index 0000000..84f61e8 --- /dev/null +++ b/georm-macros/src/georm/ir/mod.rs @@ -0,0 +1,98 @@ +use quote::quote; + +pub mod simple_relationship; +use simple_relationship::{OneToMany, OneToOne, SimpleRelationship}; + +pub mod m2m_relationship; +use m2m_relationship::M2MRelationship; + +#[derive(deluxe::ExtractAttributes)] +#[deluxe(attributes(georm))] +pub struct GeormStructAttributes { + pub table: String, + #[deluxe(default = Vec::new())] + pub one_to_one: Vec>, + #[deluxe(default = Vec::new())] + pub one_to_many: Vec>, + #[deluxe(default = Vec::new())] + pub many_to_many: Vec, +} + +#[derive(deluxe::ExtractAttributes, Clone)] +#[deluxe(attributes(georm))] +struct GeormFieldAttributes { + #[deluxe(default = false)] + pub id: bool, + #[deluxe(default = None)] + pub relation: Option, +} + +#[derive(deluxe::ParseMetaItem, Clone, Debug)] +pub struct O2ORelationship { + pub entity: syn::Type, + pub table: String, + #[deluxe(default = String::from("id"))] + pub remote_id: String, + #[deluxe(default = false)] + pub nullable: bool, + pub name: String, +} + +#[derive(Clone, Debug)] +pub struct GeormField { + pub ident: syn::Ident, + pub field: syn::Field, + pub ty: syn::Type, + pub id: bool, + pub relation: Option, +} + +impl GeormField { + pub fn new(field: &mut syn::Field) -> Self { + let ident = field.clone().ident.unwrap(); + let ty = field.clone().ty; + let attrs: GeormFieldAttributes = + deluxe::extract_attributes(field).expect("Could not extract attributes from field"); + let GeormFieldAttributes { id, relation } = attrs; + Self { + ident, + field: field.to_owned(), + id, + ty, + relation, + } + } +} + +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(), + ); + let entity = &relation.entity; + let return_type = if relation.nullable { + quote! { Option<#entity> } + } else { + quote! { #entity } + }; + let query = format!( + "SELECT * FROM {} WHERE {} = $1", + relation.table, relation.remote_id + ); + let local_ident = &value.field.ident; + let fetch = if relation.nullable { + quote! { fetch_optional } + } else { + quote! { fetch_one } + }; + quote! { + 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/ir/simple_relationship.rs b/georm-macros/src/georm/ir/simple_relationship.rs new file mode 100644 index 0000000..5046068 --- /dev/null +++ b/georm-macros/src/georm/ir/simple_relationship.rs @@ -0,0 +1,66 @@ +use quote::quote; + +pub trait SimpleRelationshipType {} + +#[derive(deluxe::ParseMetaItem, Default)] +pub struct OneToOne; +impl SimpleRelationshipType for OneToOne {} + +#[derive(deluxe::ParseMetaItem, Default)] +pub struct OneToMany; +impl SimpleRelationshipType for OneToMany {} + +#[derive(deluxe::ParseMetaItem)] +pub struct SimpleRelationship +where + T: SimpleRelationshipType + deluxe::ParseMetaItem + Default, +{ + pub name: String, + pub remote_id: String, + pub table: String, + pub entity: syn::Type, + #[deluxe(default = T::default())] + _phantom: T, +} + +impl SimpleRelationship +where + T: SimpleRelationshipType + deluxe::ParseMetaItem + Default, +{ + pub fn make_query(&self) -> String { + format!("SELECT * FROM {} WHERE {} = $1", self.table, self.remote_id) + } + + pub fn make_function_name(&self) -> syn::Ident { + syn::Ident::new( + &format!("get_{}", self.name), + proc_macro2::Span::call_site(), + ) + } +} + +impl From<&SimpleRelationship> for proc_macro2::TokenStream { + fn from(value: &SimpleRelationship) -> Self { + let query = value.make_query(); + let entity = &value.entity; + let function = value.make_function_name(); + quote! { + pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result> { + ::sqlx::query_as!(#entity, #query, self.get_id()).fetch_optional(pool).await + } + } + } +} + +impl From<&SimpleRelationship> for proc_macro2::TokenStream { + fn from(value: &SimpleRelationship) -> Self { + let query = value.make_query(); + let entity = &value.entity; + let function = value.make_function_name(); + quote! { + pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result> { + ::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await + } + } + } +} diff --git a/georm-macros/src/georm/relationships.rs b/georm-macros/src/georm/relationships.rs index f51c47b..0ac318c 100644 --- a/georm-macros/src/georm/relationships.rs +++ b/georm-macros/src/georm/relationships.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use crate::georm::ir::M2MRelationshipComplete; +use crate::georm::ir::m2m_relationship::M2MRelationshipComplete; use super::ir::GeormField; use proc_macro2::TokenStream; @@ -15,16 +15,12 @@ fn join_token_streams(token_streams: &[TokenStream]) -> TokenStream { .collect() } -fn derive(relationships: &[T], condition: P) -> TokenStream +fn derive(relationships: &[T]) -> TokenStream where for<'a> &'a T: Into, - P: FnMut(&&T) -> bool, { - let implementations: Vec = relationships - .iter() - .filter(condition) - .map(std::convert::Into::into) - .collect(); + let implementations: Vec = + relationships.iter().map(std::convert::Into::into).collect(); join_token_streams(&implementations) } @@ -35,18 +31,20 @@ pub fn derive_relationships( id: &GeormField, ) -> TokenStream { let struct_name = &ast.ident; - 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_one_local = derive(fields); + let one_to_one_remote = derive(&struct_attrs.one_to_one); + let one_to_many = derive(&struct_attrs.one_to_many); let many_to_many: Vec = struct_attrs .many_to_many .iter() .map(|v| M2MRelationshipComplete::new(v, &struct_attrs.table, id.ident.to_string())) .collect(); - let many_to_many = derive(&many_to_many, |_| true); + let many_to_many = derive(&many_to_many); quote! { impl #struct_name { - #one_to_one + #one_to_one_local + #one_to_one_remote #one_to_many #many_to_many } diff --git a/src/lib.rs b/src/lib.rs index 3a24a72..caa6b30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,13 +58,13 @@ //! //! Here is an explanation of what these different values mean: //! -//! | Value Name | Explanation | Default value | -//! |------------|-----------------------------------------------------------------------------------------|---------------| -//! | entity | Rust type of the entity found in the database | N/A | +//! | Value Name | Explanation | Default value | +//! |------------|------------------------------------------------------------------------------------------|---------------| +//! | entity | Rust type of the entity found in the database | N/A | //! | name | Name of the remote entity within the local entity; generates a method named `get_{name}` | N/A | -//! | table | Database table where the entity is stored | N/A | -//! | remote_id | Name of the column serving as the identifier of the entity | `"id"` | -//! | nullable | Whether the relationship can be broken | `false` | +//! | table | Database table where the entity is stored | N/A | +//! | remote_id | Name of the column serving as the identifier of the entity | `"id"` | +//! | nullable | Whether the relationship can be broken | `false` | //! //! Note that in this instance, the `remote_id` and `nullable` values can be //! omitted as this is their default value. This below is a strict equivalent: @@ -81,6 +81,39 @@ //! } //! ``` //! +//! But what if I have a one-to-one relationship with another entity and +//! my current entity holds no data to reference that other identity? No +//! worries, there is another way to declare such relationships. +//! +//! ```ignore +//! #[georm( +//! one_to_one = [{ +//! name = "profile", +//! remote_id = "user_id", +//! table = "profiles", +//! entity = User +//! }] +//! )] +//! struct User { +//! #[georm(id)] +//! id: i32, +//! username: String, +//! hashed_password: String, +//! } +//! ``` +//! +//! We now have access to the method `User::get_profile(&self, &pool: +//! sqlx::PgPool) -> Option`. +//! +//! Here is an explanation of the values of `one_to_many`: +//! +//! | Value Name | Explanaion | Default Value | +//! |------------|------------------------------------------------------------------------------------------|---------------| +//! | entity | Rust type of the entity found in the database | N/A | +//! | name | Name of the remote entity within the local entity; generates a method named `get_{name}` | N/A | +//! | table | Database table where the entity is stored | N/A | +//! | remote_id | Name of the column serving as the identifier of the entity | `"id"` | +//! //! ## One-to-many relationships //! //! Sometimes, our entity is the one being referenced to by multiple entities, @@ -105,7 +138,7 @@ //! entity = Post, //! name = "posts", //! table = "posts", -//! remote_id = "id" +//! remote_id = "author_id" //! }] //! )] //! struct User { diff --git a/tests/fixtures/simple_struct.sql b/tests/fixtures/simple_struct.sql index e280e38..3da6929 100644 --- a/tests/fixtures/simple_struct.sql +++ b/tests/fixtures/simple_struct.sql @@ -1,6 +1,7 @@ INSERT INTO biographies (content) VALUES ('Some text'), - ('Some other text'); + ('Some other text'), + ('Biography for no one'); INSERT INTO authors (name, biography_id) VALUES ('J.R.R. Tolkien', 2), diff --git a/tests/models.rs b/tests/models.rs index 3fb7288..0e128cb 100644 --- a/tests/models.rs +++ b/tests/models.rs @@ -1,7 +1,12 @@ use georm::Georm; #[derive(Debug, sqlx::FromRow, Georm, PartialEq, Eq, Default)] -#[georm(table = "biographies")] +#[georm( + table = "biographies", + one_to_one = [{ + name = "author", remote_id = "biography_id", table = "authors", entity = Author + }] +)] pub struct Biography { #[georm(id)] pub id: i32, diff --git a/tests/o2o_relationship.rs b/tests/o2o_relationship.rs index ba74c05..426c36c 100644 --- a/tests/o2o_relationship.rs +++ b/tests/o2o_relationship.rs @@ -53,3 +53,24 @@ async fn books_are_found_despite_nonstandard_id_name(pool: sqlx::PgPool) -> sqlx assert_eq!(tolkien, book.get_author(&pool).await?); Ok(()) } + +#[sqlx::test(fixtures("simple_struct"))] +async fn biographies_should_find_remote_o2o_author(pool: sqlx::PgPool) -> sqlx::Result<()> { + let london = Author::find(&pool, &3).await?.unwrap(); + let london_biography = Biography::find(&pool, &1).await?.unwrap(); + let result = london_biography.get_author(&pool).await; + assert!(result.is_ok()); + let result = result.unwrap(); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(london, result); + Ok(()) +} + +#[sqlx::test(fixtures("simple_struct"))] +async fn biographies_may_not_have_corresponding_author(pool: sqlx::PgPool) -> sqlx::Result<()> { + let biography = Biography::find(&pool, &3).await?.unwrap(); + let result = biography.get_author(&pool).await?; + assert!(result.is_none()); + Ok(()) +}