diff --git a/Cargo.lock b/Cargo.lock index d5c9a78..e30cc72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -986,6 +986,8 @@ dependencies = [ [[package]] name = "georm" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2296991bbb46079284784ac80c300459f9f5f1dcfed9bc17922d70501d219c9d" dependencies = [ "georm-macros", "sqlx", @@ -994,6 +996,8 @@ dependencies = [ [[package]] name = "georm-macros" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b78c25c3daa9504cf060da15b69e27591267fb2b6d5123d06770f0bf11e8146" dependencies = [ "deluxe", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index fc2f6cd..5791faa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,7 @@ [workspace] - members = [ "gejdr-core", "gejdr-bot", - "gejdr-backend", - "georm-macros", - "georm" + "gejdr-backend" ] resolver = "2" diff --git a/gejdr-bot/Cargo.toml b/gejdr-bot/Cargo.toml index c69aabf..dfe6680 100644 --- a/gejdr-bot/Cargo.toml +++ b/gejdr-bot/Cargo.toml @@ -2,5 +2,7 @@ name = "gejdr-bot" version = "0.1.0" edition = "2021" +publish = false +authors = ["Lucien Cartier-Tilet "] [dependencies] diff --git a/gejdr-core/Cargo.toml b/gejdr-core/Cargo.toml index a9f6ac9..888b0fe 100644 --- a/gejdr-core/Cargo.toml +++ b/gejdr-core/Cargo.toml @@ -2,14 +2,19 @@ name = "gejdr-core" version = "0.1.0" edition = "2021" +publish = false +authors = ["Lucien Cartier-Tilet "] [dependencies] chrono = { version = "0.4.38", features = ["serde"] } serde = "1.0.215" 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"] } -georm = { path = "../georm" } +georm = "0.1" + +[dependencies.tracing-subscriber] +version = "0.3.18" +features = ["fmt", "std", "env-filter", "registry", "json", "tracing-log"] [dependencies.sqlx] version = "0.8.3" diff --git a/georm-macros/Cargo.toml b/georm-macros/Cargo.toml deleted file mode 100644 index 16cde4d..0000000 --- a/georm-macros/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "georm-macros" -version = "0.1.0" -edition = "2021" -authors = ["Lucien Cartier-Tilet "] -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" \ No newline at end of file diff --git a/georm-macros/src/georm/ir.rs b/georm-macros/src/georm/ir.rs deleted file mode 100644 index 42a2af8..0000000 --- a/georm-macros/src/georm/ir.rs +++ /dev/null @@ -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, - #[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> { - 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> { - 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, - #[deluxe(default = None)] - pub relation: Option, -} - -// #[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, -} - -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 - } - } - } -} diff --git a/georm-macros/src/georm/mod.rs b/georm-macros/src/georm/mod.rs deleted file mode 100644 index 9e3a1de..0000000 --- a/georm-macros/src/georm/mod.rs +++ /dev/null @@ -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)> { - 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::>(); - let identifiers: Vec = 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 { - 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) -} diff --git a/georm-macros/src/georm/relationships.rs b/georm-macros/src/georm/relationships.rs deleted file mode 100644 index d88f7b6..0000000 --- a/georm-macros/src/georm/relationships.rs +++ /dev/null @@ -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(relationships: &[T], condition: P) -> TokenStream -where - for<'a> &'a T: Into, - P: FnMut(&&T) -> bool, -{ - let implementations: Vec = 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 = 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 - } - } -} diff --git a/georm-macros/src/georm/trait_implementation.rs b/georm-macros/src/georm/trait_implementation.rs deleted file mode 100644 index 5d6352f..0000000 --- a/georm-macros/src/georm/trait_implementation.rs +++ /dev/null @@ -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::>().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::>() - .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> { - ::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 = (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::>() - .join(", "), - inputs.join(", ") - ); - let field_idents: Vec = fields.iter().map(|f| f.ident.clone()).collect(); - quote! { - async fn create(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { - ::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::>() - .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 { - ::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 { - 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 { - 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 { - if Self::find(pool, &self.#id_ident).await?.is_some() { - self.update(pool).await - } else { - self.create(pool).await - } - } - - #delete_query - } - } -} diff --git a/georm-macros/src/lib.rs b/georm-macros/src/lib.rs deleted file mode 100644 index 73b9892..0000000 --- a/georm-macros/src/lib.rs +++ /dev/null @@ -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 = ...` -//! - insert an entity into the database -//! -//! SQL query: `INSERT INTO ... (...) VALUES (...) RETURNING *` -//! - update an entity in the database -//! -//! SQL query: `UPDATE ... SET ... WHERE = ... RETURNING *` -//! - delete an entity from the database using its id or an id -//! provided by the interface’s user -//! -//! SQL query: `DELETE FROM ... WHERE = ...` -//! - 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() -} diff --git a/georm/Cargo.toml b/georm/Cargo.toml deleted file mode 100644 index 1d7f8b1..0000000 --- a/georm/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "georm" -version = "0.1.0" -edition = "2021" -authors = ["Lucien Cartier-Tilet "] -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" \ No newline at end of file diff --git a/georm/README.md b/georm/README.md deleted file mode 100644 index 407c50b..0000000 --- a/georm/README.md +++ /dev/null @@ -1 +0,0 @@ -# A small, opiniated ORM for SQLx with PostgreSQL diff --git a/georm/src/lib.rs b/georm/src/lib.rs deleted file mode 100644 index a71a9de..0000000 --- a/georm/src/lib.rs +++ /dev/null @@ -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 { - /// 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>> + 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> + 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> + 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> + 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> + 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> + Send; - - /// Returns the identifier of the entity. - fn get_id(&self) -> &Id; -} diff --git a/georm/tests/fixtures/simple_struct.sql b/georm/tests/fixtures/simple_struct.sql deleted file mode 100644 index e69de29..0000000 diff --git a/georm/tests/simple_struct.rs b/georm/tests/simple_struct.rs deleted file mode 100644 index f6a069f..0000000 --- a/georm/tests/simple_struct.rs +++ /dev/null @@ -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 -}