This commit is contained in:
elliot speck 2024-12-29 14:08:28 +11:00
parent d3a5a8d74c
commit a16ad316a3
No known key found for this signature in database
8 changed files with 136 additions and 18 deletions

90
Cargo.lock generated
View file

@ -2,6 +2,16 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
dependencies = [
"lazy_static",
"regex",
]
[[package]]
name = "ahash"
version = "0.8.11"
@ -14,12 +24,32 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "async-trait"
version = "0.1.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atoi"
version = "2.0.0"
@ -282,7 +312,9 @@ dependencies = [
name = "ezpg"
version = "0.0.0"
dependencies = [
"async-trait",
"ezpg_macros",
"futures",
"sqlx",
"uuid",
]
@ -291,6 +323,7 @@ dependencies = [
name = "ezpg_macros"
version = "0.0.0"
dependencies = [
"Inflector",
"darling",
"itertools",
"proc-macro2",
@ -330,6 +363,21 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@ -374,6 +422,17 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
@ -392,8 +451,10 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
@ -957,6 +1018,35 @@ dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rsa"
version = "0.9.7"

View file

@ -7,6 +7,7 @@ edition = "2021"
proc-macro = true
[dependencies]
Inflector = "0.11.4"
darling = "0.20.10"
itertools = "0.13.0"
proc-macro2 = "1.0.92"

View file

@ -23,8 +23,7 @@ impl Column {
/// returns the column's name, either the `rename` attribute, or the
/// column name itself.
pub fn name(&self) -> String {
self
.rename
self.rename
.as_ref()
.cloned()
.unwrap_or(self.ident().to_string())

View file

@ -1,8 +1,9 @@
use crate::Column;
use darling::{ast::Data, FromDeriveInput};
use inflector::Inflector;
use itertools::Itertools;
use quote::quote;
use syn::Ident;
use crate::Column;
#[derive(FromDeriveInput)]
#[darling(attributes(sqlx), supports(struct_named))]
@ -17,7 +18,7 @@ impl RecordDerive {
self.rename
.as_ref()
.cloned()
.unwrap_or(self.ident.to_string().to_lowercase())
.unwrap_or(self.ident.to_string().to_plural().to_snake_case())
}
// returns all columns that are not marked `skip`.
@ -52,16 +53,17 @@ impl RecordDerive {
// e.g., "select id, name, weight from items limit $1 offset $2"
let index_query =
format!("select id, {column_names_join} from {table_name} limit $1 offset $2");
format!("select {column_names_join} from {table_name} limit $1 offset $2");
// e.g., "select id, name, weight from items where id = $1"
let read_query = format!("select id, {column_names_join} from {table_name} where id = $1");
let read_query = format!("select {column_names_join} from {table_name} where id = $1");
let update_query = {
let value_pairs = self
.columns()
.enumerate()
.map(|(i, col)| format!("{} = ${}", col.name(), i + 2)) // enum starts at 0, id is 1, other columns start at 2.
.filter(|(_, c)| c.name() != "id")
.map(|(i, col)| format!("{} = ${}", col.name(), i + 1)) // enum starts at 0, id is 1, other columns start at 2.
.join(",");
format!("update {table_name} set {value_pairs} where id = $1")
@ -70,6 +72,12 @@ impl RecordDerive {
let delete_query = format!("delete from {table_name} where id = $1");
quote! {
impl ::ezpg::Record for #ident {
fn id(&self) -> uuid::Uuid {
self.id
}
}
impl ::ezpg::WithQueries for #ident {
const COLUMNS: &'static [&'static str] = &[#(#column_names,)*];
const CREATE_QUERY: &'static str = #create_query;

View file

@ -3,8 +3,8 @@ pub(crate) use column::Column;
mod derive;
use darling::FromDeriveInput;
use proc_macro::TokenStream;
use syn::DeriveInput;
use quote::quote;
use syn::DeriveInput;
#[proc_macro_derive(Record, attributes(sqlx))]
pub fn derive_record(input: TokenStream) -> TokenStream {
@ -17,6 +17,7 @@ pub fn derive_record(input: TokenStream) -> TokenStream {
quote! {
#queries_impl
#column_impl
}
.into()

View file

@ -4,6 +4,8 @@ version = "0.0.0"
edition = "2021"
[dependencies]
async-trait = "0.1.83"
ezpg_macros = { version = "0.0.0", path = "../ezpg-macros" }
futures = "0.3.31"
sqlx = { version = "0.8.2", features = ["macros", "postgres", "uuid"] }
uuid = { version = "1.11.0", features = ["v7"] }

View file

@ -1,15 +1,17 @@
//! traits used for c/r/u/d behaviours.
use crate::Record;
use async_trait::async_trait;
use sqlx::{PgPool, Postgres};
use uuid::Uuid;
use crate::Record;
/// [c]reate
#[async_trait]
pub trait Create<'a, R> {
async fn create(&self, pool: &PgPool) -> sqlx::Result<R>;
}
#[async_trait]
impl<'a, R> Create<'a, R> for R
where
R: Record,
@ -35,11 +37,13 @@ where
}
/// [r]ead
#[async_trait]
pub trait Read<'a, R>: Record {
async fn index(offset: i64, limit: i64, pool: &PgPool) -> sqlx::Result<Vec<R>>;
async fn read(id: Uuid, pool: &PgPool) -> sqlx::Result<R>;
async fn find_by_id(id: Uuid, pool: &PgPool) -> sqlx::Result<R>;
}
#[async_trait]
impl<'a, R> Read<'a, R> for R
where
R: Record,
@ -53,7 +57,7 @@ where
.await
}
async fn read(id: Uuid, pool: &PgPool) -> sqlx::Result<R> {
async fn find_by_id(id: Uuid, pool: &PgPool) -> sqlx::Result<R> {
sqlx::query_as::<Postgres, R>(R::READ_QUERY)
.bind(id)
.fetch_one(pool)
@ -62,10 +66,12 @@ where
}
/// [c]reate
#[async_trait]
pub trait Update<'a, R> {
async fn update(&self, pool: &PgPool) -> sqlx::Result<R>;
}
#[async_trait]
impl<'a, R> Update<'a, R> for R
where
R: Record,
@ -74,19 +80,24 @@ where
async fn update(&self, pool: &PgPool) -> sqlx::Result<R> {
R::COLUMNS
.iter()
.fold(sqlx::query_as(R::UPDATE_QUERY), |q, c| {
self.bind_column(q, c)
})
.fold(sqlx::query(R::UPDATE_QUERY), |q, c| self.bind_column(q, c))
.execute(pool)
.await?;
sqlx::query_as::<Postgres, R>(R::READ_QUERY)
.bind(self.id())
.fetch_one(pool)
.await
}
}
/// [d]elete
#[async_trait]
pub trait Delete<'a, R> {
async fn delete(&self, pool: &PgPool) -> sqlx::Result<()>;
}
#[async_trait]
impl<'a, R> Delete<'a, R> for R
where
R: Record,

View file

@ -1,11 +1,15 @@
mod crud_traits;
pub use crud_traits::{Create, Delete, Read, Update};
use sqlx::{postgres::{PgArguments, PgRow}, query::{Query, QueryAs}, Encode, FromRow, Postgres, Type};
use sqlx::{
postgres::{PgArguments, PgRow},
query::{Query, QueryAs},
Encode, FromRow, Postgres, Type,
};
use uuid::Uuid;
// re-export the record macro.
pub use ezpg_macros::Record;
// the following were stolen from [miniorm by meuter](https://github.com/meuter/miniorm-rs):
pub trait QueryBind<'q> {
fn bind<T>(self, value: T) -> Self
@ -49,6 +53,8 @@ pub trait BindColumn {
}
/// a record stored within the postgres db.
pub trait Record: WithQueries + BindColumn + for<'r> FromRow<'r, PgRow> + Send + Unpin {
pub trait Record:
WithQueries + BindColumn + for<'r> FromRow<'r, PgRow> + Send + Sync + Unpin
{
fn id(&self) -> Uuid;
}