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..9c313e9 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,10 @@
A simple, opinionated SQLx ORM for PostgreSQL
+
+
+
-
@@ -13,26 +15,22 @@
-
-
+
+
-
+
+
-
-
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 +38,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 +115,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 +124,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(())
+}