Compare commits

..

1 Commits

Author SHA1 Message Date
91d7651eca
feat: add foreign one_to_one relationships
All checks were successful
CI / tests (push) Successful in 4m20s
2025-03-02 16:12:20 +01:00
13 changed files with 1669 additions and 293 deletions

111
Cargo.lock generated
View File

@ -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",

View File

@ -1,10 +1,15 @@
<div align="center">
<a href="https://github.com/Phundrak/georm">
<img src="assets/logo.png" alt="Georm logo" width="150px" />
</a>
</div>
<h1 align="center">Georm</h1>
<div align="center">
<strong>
A simple, opinionated SQLx ORM for PostgreSQL
</strong>
</div>
<br/>
<div align="center">
@ -13,26 +18,22 @@
<img src="https://img.shields.io/github/actions/workflow/status/phundrak/georm/ci.yaml?branch=main&style=flat-square" alt="actions status" /></a>
<!-- Version -->
<a href="https://crates.io/crates/georm">
<img src="https://img.shields.io/crates/v/georm.svg?style=flat-square"
alt="Crates.io version" /></a>
<!-- Discord -->
<img src="https://img.shields.io/crates/v/georm.svg?style=flat-square" alt="Crates.io version" />
</a>
<!-- Docs -->
<a href="https://docs.rs/georm">
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square" alt="docs.rs docs" /></a>
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square" alt="docs.rs docs" />
</a>
</div>
<div align="center">
<h4>What is Georm?</h4>
</div>
## 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 youre tired of writing.
<div align="center">
<h4>Why is Georm?</h4>
</div>
## Why is Georm?
I wanted an ORM thats easy and straightforward to use. I am aware
some other projects exist, such as
@ -40,16 +41,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.
<div align="center">
<h4>How is Georm?</h4>
</div>
## How is Georm?
I use it in a few projects, and Im quite happy with it right now. But
of course, Im open to constructive criticism and suggestions!
<div align="center">
<h4>How can I use it?</h4>
</div>
## 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 +118,7 @@ pub struct Author {
Congratulations, your struct `Author` now has access to all the
functions described in the `Georm` trait!
<div align="center">
<h4>Entity relationship</h4>
</div>
## 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 +127,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",

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

1272
assets/logo.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -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<O2MRelationship>,
#[deluxe(default = Vec::new())]
pub many_to_many: Vec<M2MRelationship>,
}
#[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<Vec<#entity>> {
::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<Vec<#entity>> {
::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<O2ORelationship>,
}
#[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<O2ORelationship>,
}
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
}
}
}
}

View File

@ -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<Vec<#entity>> {
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await
}
}
}
}

View File

@ -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<SimpleRelationship<OneToOne>>,
#[deluxe(default = Vec::new())]
pub one_to_many: Vec<SimpleRelationship<OneToMany>>,
#[deluxe(default = Vec::new())]
pub many_to_many: Vec<M2MRelationship>,
}
#[derive(deluxe::ExtractAttributes, Clone)]
#[deluxe(attributes(georm))]
struct GeormFieldAttributes {
#[deluxe(default = false)]
pub id: bool,
#[deluxe(default = None)]
pub relation: Option<O2ORelationship>,
}
#[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<O2ORelationship>,
}
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
}
}
}
}

View File

@ -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<T>
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<T> SimpleRelationship<T>
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<OneToOne>> for proc_macro2::TokenStream {
fn from(value: &SimpleRelationship<OneToOne>) -> 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<Option<#entity>> {
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_optional(pool).await
}
}
}
}
impl From<&SimpleRelationship<OneToMany>> for proc_macro2::TokenStream {
fn from(value: &SimpleRelationship<OneToMany>) -> 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<Vec<#entity>> {
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await
}
}
}
}

View File

@ -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<T, P>(relationships: &[T], condition: P) -> TokenStream
fn derive<T>(relationships: &[T]) -> TokenStream
where
for<'a> &'a T: Into<TokenStream>,
P: FnMut(&&T) -> bool,
{
let implementations: Vec<TokenStream> = relationships
.iter()
.filter(condition)
.map(std::convert::Into::into)
.collect();
let implementations: Vec<TokenStream> =
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<M2MRelationshipComplete> = 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
}

View File

@ -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<User>`.
//!
//! 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 {

View File

@ -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),

View File

@ -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,

View File

@ -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(())
}