generated from phundrak/rust-poem-openapi-template
chore: georm is now part of a separate crate
Some checks failed
CI / tests (push) Failing after 5m34s
Some checks failed
CI / tests (push) Failing after 5m34s
This commit is contained in:
parent
857b1d98d0
commit
78484c1a6f
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -986,6 +986,8 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "georm"
|
name = "georm"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2296991bbb46079284784ac80c300459f9f5f1dcfed9bc17922d70501d219c9d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"georm-macros",
|
"georm-macros",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
@ -994,6 +996,8 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "georm-macros"
|
name = "georm-macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b78c25c3daa9504cf060da15b69e27591267fb2b6d5123d06770f0bf11e8146"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deluxe",
|
"deluxe",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
|
|
||||||
members = [
|
members = [
|
||||||
"gejdr-core",
|
"gejdr-core",
|
||||||
"gejdr-bot",
|
"gejdr-bot",
|
||||||
"gejdr-backend",
|
"gejdr-backend"
|
||||||
"georm-macros",
|
|
||||||
"georm"
|
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
@ -2,5 +2,7 @@
|
|||||||
name = "gejdr-bot"
|
name = "gejdr-bot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
authors = ["Lucien Cartier-Tilet <lucien@phundrak.com>"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
@ -2,14 +2,19 @@
|
|||||||
name = "gejdr-core"
|
name = "gejdr-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
authors = ["Lucien Cartier-Tilet <lucien@phundrak.com>"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
serde = "1.0.215"
|
serde = "1.0.215"
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt", "std", "env-filter", "registry", "json", "tracing-log"] }
|
|
||||||
uuid = { version = "1.11.0", features = ["v4", "serde"] }
|
uuid = { version = "1.11.0", features = ["v4", "serde"] }
|
||||||
georm = { path = "../georm" }
|
georm = "0.1"
|
||||||
|
|
||||||
|
[dependencies.tracing-subscriber]
|
||||||
|
version = "0.3.18"
|
||||||
|
features = ["fmt", "std", "env-filter", "registry", "json", "tracing-log"]
|
||||||
|
|
||||||
[dependencies.sqlx]
|
[dependencies.sqlx]
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "georm-macros"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
authors = ["Lucien Cartier-Tilet <lucien@phundrak.com>"]
|
|
||||||
description = "Macros for Georm, the small, opiniated SQLx ORM for PostgreSQL. Not intended to be used directly."
|
|
||||||
homepage = "https://labs.phundrak.com/phundrak/gejdr-rs"
|
|
||||||
repository = "https://labs.phundrak.com/phundrak/gejdr-rs"
|
|
||||||
license = "MIT OR GPL-3.0-or-later"
|
|
||||||
keywords = ["sqlx", "orm", "postgres", "postgresql", "database", "async"]
|
|
||||||
categories = ["database"]
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
proc-macro = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
deluxe = "0.5.0"
|
|
||||||
proc-macro2 = "1.0.93"
|
|
||||||
quote = "1.0.38"
|
|
||||||
syn = "2.0.96"
|
|
||||||
|
|
||||||
[lints.rust]
|
|
||||||
unsafe_code = "forbid"
|
|
@ -1,228 +0,0 @@
|
|||||||
use quote::quote;
|
|
||||||
use std::fmt::{self, Display};
|
|
||||||
|
|
||||||
#[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>> {
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
//#[georm(
|
|
||||||
// table = "users",
|
|
||||||
// many_to_many = [
|
|
||||||
// {
|
|
||||||
// name = friends,
|
|
||||||
// entity: User,
|
|
||||||
// link = { table = "user_friendships", from: "user1", to "user2" }
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
//)]
|
|
||||||
#[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.to_string(),
|
|
||||||
},
|
|
||||||
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>> {
|
|
||||||
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 column: Option<String>,
|
|
||||||
#[deluxe(default = None)]
|
|
||||||
pub relation: Option<O2ORelationship>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[georm(
|
|
||||||
// table = "profileId",
|
|
||||||
// one_to_one = { name = profile, id = "id", entity = Profile, nullable }
|
|
||||||
// )]
|
|
||||||
#[derive(deluxe::ParseMetaItem, Clone)]
|
|
||||||
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)]
|
|
||||||
pub struct GeormField {
|
|
||||||
pub ident: syn::Ident,
|
|
||||||
pub field: syn::Field,
|
|
||||||
pub ty: syn::Type,
|
|
||||||
pub column: String,
|
|
||||||
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");
|
|
||||||
Self {
|
|
||||||
ident: ident.clone(),
|
|
||||||
field: field.to_owned(),
|
|
||||||
column: attrs.column.unwrap_or_else(|| ident.to_string()),
|
|
||||||
id: attrs.id,
|
|
||||||
ty,
|
|
||||||
relation: attrs.relation,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for GeormField {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
let default_column = self.ident.to_string();
|
|
||||||
if self.column == default_column {
|
|
||||||
write!(f, "{}", self.column)
|
|
||||||
} else {
|
|
||||||
write!(f, "{} as {}", self.column, default_column)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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(&value, pool: &::sqlx::PgPool) -> ::sqlx::Result<#return_type> {
|
|
||||||
query_as!(#entity, #query, value.#local_ident).#fetch(pool).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
use ir::GeormField;
|
|
||||||
use quote::quote;
|
|
||||||
|
|
||||||
mod ir;
|
|
||||||
mod relationships;
|
|
||||||
mod trait_implementation;
|
|
||||||
|
|
||||||
fn extract_georm_field_attrs(
|
|
||||||
ast: &mut syn::DeriveInput,
|
|
||||||
) -> deluxe::Result<(Vec<GeormField>, GeormField)> {
|
|
||||||
let syn::Data::Struct(s) = &mut ast.data else {
|
|
||||||
return Err(syn::Error::new_spanned(
|
|
||||||
ast,
|
|
||||||
"Cannot apply to something other than a struct",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
let fields = s
|
|
||||||
.fields
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.map(|mut field| GeormField::new(&mut field))
|
|
||||||
.collect::<Vec<GeormField>>();
|
|
||||||
let identifiers: Vec<GeormField> = fields
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|field| field.id)
|
|
||||||
.collect();
|
|
||||||
match identifiers.len() {
|
|
||||||
0 => Err(syn::Error::new_spanned(
|
|
||||||
ast,
|
|
||||||
"Struct {name} must have one identifier",
|
|
||||||
)),
|
|
||||||
1 => Ok((fields, identifiers.first().unwrap().clone())),
|
|
||||||
_ => {
|
|
||||||
let id1 = identifiers.first().unwrap();
|
|
||||||
let id2 = identifiers.get(1).unwrap();
|
|
||||||
Err(syn::Error::new_spanned(id2.field.clone(), format!(
|
|
||||||
"Field {} cannot be an identifier, {} already is one.\nOnly one identifier is supported.",
|
|
||||||
id1.ident, id2.ident
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn georm_derive_macro2(
|
|
||||||
item: proc_macro2::TokenStream,
|
|
||||||
) -> deluxe::Result<proc_macro2::TokenStream> {
|
|
||||||
let mut ast: syn::DeriveInput = syn::parse2(item).expect("Failed to parse input");
|
|
||||||
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 code = quote! {
|
|
||||||
#trait_impl
|
|
||||||
#relationships
|
|
||||||
};
|
|
||||||
Ok(code)
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::georm::ir::M2MRelationshipComplete;
|
|
||||||
|
|
||||||
use super::ir::GeormField;
|
|
||||||
use proc_macro2::TokenStream;
|
|
||||||
use quote::quote;
|
|
||||||
|
|
||||||
fn join_token_streams(token_streams: &[TokenStream]) -> TokenStream {
|
|
||||||
let newline = TokenStream::from_str("\n").unwrap();
|
|
||||||
token_streams
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.flat_map(|ts| std::iter::once(ts).chain(std::iter::once(newline.clone())))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derive<T, P>(relationships: &[T], condition: P) -> 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();
|
|
||||||
join_token_streams(&implementations)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn derive_relationships(
|
|
||||||
ast: &syn::DeriveInput,
|
|
||||||
struct_attrs: &super::ir::GeormStructAttributes,
|
|
||||||
fields: &[GeormField],
|
|
||||||
id: &GeormField,
|
|
||||||
) -> TokenStream {
|
|
||||||
let struct_name = &ast.ident;
|
|
||||||
let one_to_one = derive(fields, |field| field.relation.is_none());
|
|
||||||
let one_to_many = derive(&struct_attrs.one_to_many, |_| true);
|
|
||||||
let many_to_many: Vec<M2MRelationshipComplete> = struct_attrs
|
|
||||||
.many_to_many
|
|
||||||
.iter()
|
|
||||||
.map(|v| M2MRelationshipComplete::new(v, &struct_attrs.table, &id.column))
|
|
||||||
.collect();
|
|
||||||
let many_to_many = derive(&many_to_many, |_| true);
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
impl #struct_name {
|
|
||||||
#one_to_one
|
|
||||||
#one_to_many
|
|
||||||
#many_to_many
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,158 +0,0 @@
|
|||||||
use super::ir::GeormField;
|
|
||||||
use quote::quote;
|
|
||||||
|
|
||||||
fn aliased_columns(fields: &[GeormField]) -> String {
|
|
||||||
fields.iter().map(std::string::ToString::to_string).collect::<Vec<String>>().join(", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn generate_find_query(
|
|
||||||
table: &str,
|
|
||||||
id: &GeormField,
|
|
||||||
fields: &[GeormField],
|
|
||||||
) -> proc_macro2::TokenStream {
|
|
||||||
let select_columns = fields
|
|
||||||
.iter()
|
|
||||||
.map(std::string::ToString::to_string)
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ");
|
|
||||||
let find_string = format!(
|
|
||||||
"SELECT {select_columns} FROM {table} WHERE {} = $1",
|
|
||||||
id.column
|
|
||||||
);
|
|
||||||
let ty = &id.ty;
|
|
||||||
quote! {
|
|
||||||
async fn find(pool: &::sqlx::PgPool, id: &#ty) -> ::sqlx::Result<Option<Self>> {
|
|
||||||
::sqlx::query_as!(Self, #find_string, id)
|
|
||||||
.fetch_optional(pool)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_create_query(table: &str, fields: &[GeormField]) -> proc_macro2::TokenStream {
|
|
||||||
let inputs: Vec<String> = (1..=fields.len()).map(|num| format!("${num}")).collect();
|
|
||||||
let return_columns = aliased_columns(fields);
|
|
||||||
let create_string = format!(
|
|
||||||
"INSERT INTO {table} ({}) VALUES ({}) RETURNING {return_columns}",
|
|
||||||
fields
|
|
||||||
.iter()
|
|
||||||
.map(|v| v.column.clone())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", "),
|
|
||||||
inputs.join(", ")
|
|
||||||
);
|
|
||||||
let field_idents: Vec<syn::Ident> = fields.iter().map(|f| f.ident.clone()).collect();
|
|
||||||
quote! {
|
|
||||||
async fn create(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Self> {
|
|
||||||
::sqlx::query_as!(
|
|
||||||
Self,
|
|
||||||
#create_string,
|
|
||||||
#(self.#field_idents),*
|
|
||||||
)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_update_query(
|
|
||||||
table: &str,
|
|
||||||
fields: &[GeormField],
|
|
||||||
id: &GeormField,
|
|
||||||
) -> proc_macro2::TokenStream {
|
|
||||||
let return_columns = aliased_columns(fields);
|
|
||||||
let mut fields: Vec<&GeormField> = fields.iter().filter(|f| !f.id).collect();
|
|
||||||
let update_columns = fields
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, &field)| format!("{} = ${}", field.column, i + 1))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ");
|
|
||||||
let update_string = format!(
|
|
||||||
"UPDATE {table} SET {update_columns} WHERE {} = ${} RETURNING {return_columns}",
|
|
||||||
id.column,
|
|
||||||
fields.len() + 1
|
|
||||||
);
|
|
||||||
fields.push(id);
|
|
||||||
let field_idents: Vec<_> = fields.iter().map(|f| f.ident.clone()).collect();
|
|
||||||
quote! {
|
|
||||||
async fn update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Self> {
|
|
||||||
::sqlx::query_as!(
|
|
||||||
Self,
|
|
||||||
#update_string,
|
|
||||||
#(self.#field_idents),*
|
|
||||||
)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_delete_query(table: &str, id: &GeormField) -> proc_macro2::TokenStream {
|
|
||||||
let delete_string = format!("DELETE FROM {} WHERE {} = $1", table, id.column);
|
|
||||||
let ty = &id.ty;
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
async fn delete_by_id(pool: &::sqlx::PgPool, id: &#ty) -> ::sqlx::Result<u64> {
|
|
||||||
let rows_affected = ::sqlx::query!(#delete_string, id)
|
|
||||||
.execute(pool)
|
|
||||||
.await?
|
|
||||||
.rows_affected();
|
|
||||||
Ok(rows_affected)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<u64> {
|
|
||||||
Self::delete_by_id(pool, self.get_id()).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_get_id(id: &GeormField) -> proc_macro2::TokenStream {
|
|
||||||
let ident = &id.ident;
|
|
||||||
let ty = &id.ty;
|
|
||||||
quote! {
|
|
||||||
fn get_id(&self) -> &#ty {
|
|
||||||
&self.#ident
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn derive_trait(
|
|
||||||
ast: &syn::DeriveInput,
|
|
||||||
table: &str,
|
|
||||||
fields: &[GeormField],
|
|
||||||
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_id = generate_get_id(id);
|
|
||||||
let find_query = generate_find_query(table, id, fields);
|
|
||||||
let create_query = generate_create_query(table, fields);
|
|
||||||
let update_query = generate_update_query(table, fields, id);
|
|
||||||
let delete_query = generate_delete_query(table, id);
|
|
||||||
quote! {
|
|
||||||
impl #impl_generics Georm<#ty> for #ident #type_generics #where_clause {
|
|
||||||
#get_id
|
|
||||||
#find_query
|
|
||||||
#create_query
|
|
||||||
#update_query
|
|
||||||
|
|
||||||
async fn create_or_update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Self> {
|
|
||||||
if Self::find(pool, &self.#id_ident).await?.is_some() {
|
|
||||||
self.update(pool).await
|
|
||||||
} else {
|
|
||||||
self.create(pool).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#delete_query
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
#![deny(clippy::all)]
|
|
||||||
#![deny(clippy::pedantic)]
|
|
||||||
#![deny(clippy::nursery)]
|
|
||||||
#![allow(clippy::module_name_repetitions)]
|
|
||||||
#![allow(clippy::unused_async)]
|
|
||||||
#![forbid(unsafe_code)]
|
|
||||||
|
|
||||||
//! Creates ORM functionality for ``SQLx`` with `PostgreSQL`.
|
|
||||||
//!
|
|
||||||
//! This crate provides the trait implementation `Georm` which
|
|
||||||
//! generates the following ``SQLx`` queries:
|
|
||||||
//! - find an entity by id
|
|
||||||
//!
|
|
||||||
//! SQL query: `SELECT * FROM ... WHERE <id> = ...`
|
|
||||||
//! - insert an entity into the database
|
|
||||||
//!
|
|
||||||
//! SQL query: `INSERT INTO ... (...) VALUES (...) RETURNING *`
|
|
||||||
//! - update an entity in the database
|
|
||||||
//!
|
|
||||||
//! SQL query: `UPDATE ... SET ... WHERE <id> = ... RETURNING *`
|
|
||||||
//! - delete an entity from the database using its id or an id
|
|
||||||
//! provided by the interface’s user
|
|
||||||
//!
|
|
||||||
//! SQL query: `DELETE FROM ... WHERE <id> = ...`
|
|
||||||
//! - update an entity or create it if it does not already exist in
|
|
||||||
//! the database
|
|
||||||
//!
|
|
||||||
//! This macro relies on the trait `Georm` found in the `gejdr-core`
|
|
||||||
//! crate.
|
|
||||||
//!
|
|
||||||
//! To use this macro, you need to add it to the derives of the
|
|
||||||
//! struct. You will also need to define its identifier
|
|
||||||
//!
|
|
||||||
//! # Usage
|
|
||||||
//!
|
|
||||||
//! Add `#[georm(table = "my_table_name")]` atop of the structure,
|
|
||||||
//! after the `Georm` derive.
|
|
||||||
//!
|
|
||||||
//! ## Entity Identifier
|
|
||||||
//! You will also need to add `#[georm(id)]` atop of the field of your
|
|
||||||
//! struct that will be used as the identifier of your entity.
|
|
||||||
//!
|
|
||||||
//! ## Column Name
|
|
||||||
//! If the name of a field does not match the name of its related
|
|
||||||
//! column, you can use `#[georm(column = "...")]` to specify the
|
|
||||||
//! correct value.
|
|
||||||
//!
|
|
||||||
//! ```ignore
|
|
||||||
//! #[derive(Georm)]
|
|
||||||
//! #[georm(table = "users")]
|
|
||||||
//! pub struct User {
|
|
||||||
//! #[georm(id)]
|
|
||||||
//! id: String,
|
|
||||||
//! #[georm(column = "name")]
|
|
||||||
//! username: String,
|
|
||||||
//! created_at: Timestampz,
|
|
||||||
//! last_updated: Timestampz,
|
|
||||||
//! }
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! With the example of the `User` struct, this links it to the
|
|
||||||
//! `users` table of the connected database. It will use `Users.id` to
|
|
||||||
//! uniquely identify a user entity.
|
|
||||||
//!
|
|
||||||
//! # Limitations
|
|
||||||
//! ## ID
|
|
||||||
//! For now, only one identifier is supported. It does not have to be
|
|
||||||
//! a primary key, but it is strongly encouraged to use GeJDR’s Georm
|
|
||||||
//! ID on a unique and non-null column of your database schema.
|
|
||||||
//!
|
|
||||||
//! ## Database type
|
|
||||||
//!
|
|
||||||
//! For now, only the ``PostgreSQL`` syntax is supported. If you use
|
|
||||||
//! another database that uses the same syntax, you’re in luck!
|
|
||||||
//! Otherwise, pull requests to add additional syntaxes are most
|
|
||||||
//! welcome.
|
|
||||||
|
|
||||||
mod georm;
|
|
||||||
use georm::georm_derive_macro2;
|
|
||||||
|
|
||||||
/// Generates GEORM code for Sqlx for a struct.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// May panic if errors arise while parsing and generating code.
|
|
||||||
#[proc_macro_derive(Georm, attributes(georm))]
|
|
||||||
pub fn georm_derive_macro(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
|
||||||
georm_derive_macro2(item.into()).unwrap().into()
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "georm"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
authors = ["Lucien Cartier-Tilet <lucien@phundrak.com>"]
|
|
||||||
description = "A small, opiniated ORM for SQLx and PostgreSQL"
|
|
||||||
homepage = "https://labs.phundrak.com/phundrak/gejdr-rs"
|
|
||||||
repository = "https://labs.phundrak.com/phundrak/gejdr-rs"
|
|
||||||
license = "MIT OR GPL-3.0-or-later"
|
|
||||||
keywords = ["sqlx", "orm", "postgres", "postgresql", "database", "async"]
|
|
||||||
categories = ["database"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
georm-macros = { path = "../georm-macros" }
|
|
||||||
|
|
||||||
[dependencies.sqlx]
|
|
||||||
version = "0.8.3"
|
|
||||||
default-features = false
|
|
||||||
features = ["postgres", "runtime-tokio", "macros", "migrate"]
|
|
||||||
|
|
||||||
[lints.rust]
|
|
||||||
unsafe_code = "forbid"
|
|
@ -1 +0,0 @@
|
|||||||
# A small, opiniated ORM for SQLx with PostgreSQL
|
|
@ -1,82 +0,0 @@
|
|||||||
#![deny(clippy::all)]
|
|
||||||
#![deny(clippy::pedantic)]
|
|
||||||
#![deny(clippy::nursery)]
|
|
||||||
#![allow(clippy::module_name_repetitions)]
|
|
||||||
#![allow(clippy::unused_async)]
|
|
||||||
#![forbid(unsafe_code)]
|
|
||||||
|
|
||||||
pub use georm_macros::Georm;
|
|
||||||
|
|
||||||
pub trait Georm<Id> {
|
|
||||||
/// Find the entiy in the database based on its identifier.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// Returns any error Postgres may have encountered
|
|
||||||
fn find(
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
id: &Id,
|
|
||||||
) -> impl std::future::Future<Output = sqlx::Result<Option<Self>>> + Send
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
|
|
||||||
/// Create the entity in the database.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// Returns any error Postgres may have encountered
|
|
||||||
fn create(
|
|
||||||
&self,
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
|
|
||||||
/// Update an entity with a matching identifier in the database.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// Returns any error Postgres may have encountered
|
|
||||||
fn update(
|
|
||||||
&self,
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
|
|
||||||
/// Update an entity with a matching identifier in the database if
|
|
||||||
/// it exists, create it otherwise.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// Returns any error Postgres may have encountered
|
|
||||||
fn create_or_update(
|
|
||||||
&self,
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
|
|
||||||
/// Delete the entity from the database if it exists.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// Returns the amount of rows affected by the deletion.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// Returns any error Postgres may have encountered
|
|
||||||
fn delete(
|
|
||||||
&self,
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send;
|
|
||||||
|
|
||||||
/// Delete any entity with the identifier `id`.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// Returns the amount of rows affected by the deletion.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// Returns any error Postgres may have encountered
|
|
||||||
fn delete_by_id(
|
|
||||||
pool: &sqlx::PgPool,
|
|
||||||
id: &Id,
|
|
||||||
) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send;
|
|
||||||
|
|
||||||
/// Returns the identifier of the entity.
|
|
||||||
fn get_id(&self) -> &Id;
|
|
||||||
}
|
|
0
georm/tests/fixtures/simple_struct.sql
vendored
0
georm/tests/fixtures/simple_struct.sql
vendored
@ -1,9 +0,0 @@
|
|||||||
use georm::Georm;
|
|
||||||
|
|
||||||
#[derive(Debug, Georm)]
|
|
||||||
#[georm(table = "tests.authors")]
|
|
||||||
struct Author {
|
|
||||||
#[georm(column = "author_id", id)]
|
|
||||||
id: i32,
|
|
||||||
name: String
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user