feat(macros): add where clause

This commit is contained in:
2025-08-11 13:39:07 +02:00
parent a5d2eed7a0
commit 7aa66ea38c
8 changed files with 227 additions and 6 deletions

36
Cargo.lock generated
View File

@@ -794,6 +794,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro-crate"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
@@ -1214,6 +1223,7 @@ name = "sqlx-utils-macros"
version = "0.8.6"
dependencies = [
"darling",
"proc-macro-crate",
"proc-macro-error2",
"proc-macro2",
"quote",
@@ -1316,6 +1326,23 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.41"
@@ -1571,6 +1598,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
dependencies = [
"memchr",
]
[[package]]
name = "writeable"
version = "0.6.1"

View File

@@ -10,6 +10,7 @@ proc-macro = true
[dependencies]
darling = "0.21.1"
proc-macro-crate = "3.3.0"
proc-macro-error2 = "2.0.1"
proc-macro2 = "1.0.96"
quote = "1.0.40"

View File

@@ -1,8 +1,29 @@
use proc_macro::TokenStream;
mod push_to_builder;
mod where_clause;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::Ident;
#[proc_macro_derive(PushToBuilder)]
pub fn push_to_builder(input: TokenStream) -> TokenStream {
push_to_builder::push_to_builder(input.into()).into()
}
#[proc_macro_derive(WhereClause, attributes(sqlxu))]
pub fn where_clause(input: TokenStream) -> TokenStream {
where_clause::where_clause(input.into()).into()
}
fn crate_name() -> proc_macro2::TokenStream {
match proc_macro_crate::crate_name("sqlx_utils") {
Err(_) => quote! {::sqlx_utils},
Ok(proc_macro_crate::FoundCrate::Itself) => quote! {crate},
Ok(proc_macro_crate::FoundCrate::Name(e)) => {
let ident = Ident::new(&e, Span::call_site());
quote! {::#ident}
}
}
}

View File

@@ -4,6 +4,8 @@ use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Field, Ident, parse2};
use crate::crate_name;
#[derive(FromDeriveInput)]
struct PushToBuilderArgs {
ident: Ident,
@@ -23,6 +25,8 @@ pub fn push_to_builder(input: TokenStream) -> TokenStream {
let struct_name = args.ident;
let crate_name = crate_name();
let push_statements = args
.data
.take_struct()
@@ -35,7 +39,7 @@ pub fn push_to_builder(input: TokenStream) -> TokenStream {
.unwrap_or_else(|| Ident::new(&format!("{idx}"), Span::call_site()));
quote! {
::sqlx_utils::builder::PushToBuilder::push_to(self.#ident, builder);
#crate_name::builder::PushToBuilder::push_to(self.#ident, builder);
}
})
.collect::<Vec<_>>()
@@ -43,7 +47,7 @@ pub fn push_to_builder(input: TokenStream) -> TokenStream {
.unwrap_or_default();
quote! {
impl<DB> ::sqlx_utils::builder::PushToBuilder<DB> for #struct_name
impl<DB> #crate_name::builder::PushToBuilder<DB> for #struct_name
where
DB: ::sqlx::Database
{

131
macros/src/where_clause.rs Normal file
View File

@@ -0,0 +1,131 @@
use darling::{FromDeriveInput, FromField, ast::Data};
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{
DeriveInput, Ident, Path, PathSegment, Token, Type, TypePath, parse2, punctuated::Punctuated,
token::PathSep,
};
use crate::crate_name;
#[derive(FromField)]
#[darling(attributes(sqlxu))]
struct WhereClauseField {
ident: Option<Ident>,
op: Option<Type>,
rename: Option<String>,
ty: Type,
}
#[derive(FromDeriveInput)]
#[darling(attributes(sqlxu), supports(struct_named))]
struct WhereClauseArgs {
ident: Ident,
data: Data<(), WhereClauseField>,
}
pub fn where_clause(input: TokenStream) -> TokenStream {
let input: DeriveInput = match parse2(input) {
Ok(e) => e,
Err(e) => return e.into_compile_error(),
};
let args = match WhereClauseArgs::from_derive_input(&input) {
Ok(e) => e,
Err(e) => return e.write_errors(),
};
let struct_name = args.ident;
let crate_name = crate_name();
let push_clauses = args.data.take_struct().expect("Should never be `None`").into_iter().map(|e| {
let ident = e.ident.expect("Should never be `None`");
let col_name = e.rename.unwrap_or_else(|| ident.to_string());
let op = e.op.unwrap_or_else(|| {
let mut segments: Punctuated<PathSegment, Token![::]> = Punctuated::new();
segments.push_value(PathSegment::from(Ident::new("sqlx_utils", Span::call_site())));
segments.push_punct(PathSep { spans: [Span::call_site(); 2] });
segments.push_value(PathSegment::from(Ident::new("builder", Span::call_site())));
segments.push_punct(PathSep { spans: [Span::call_site(); 2] });
segments.push_value(PathSegment::from(Ident::new("expr", Span::call_site())));
segments.push_punct(PathSep { spans: [Span::call_site(); 2] });
segments.push_value(PathSegment::from(Ident::new("Equals", Span::call_site())));
Type::Path(TypePath {
qself: None,
path: Path {
leading_colon: Some(PathSep {
spans: [Span::call_site(); 2],
}),
segments,
},
})
});
let is_option = if let Type::Path(ty) = &e.ty {
ty.path.segments.first().is_some_and(|e| e.ident.eq("Option"))
} else {
false
};
let push_quote = quote! {
let expr = #crate_name::builder::BracketsExpr::new(
#crate_name::builder::BinaryExpr::new(#col_name, #crate_name::builder::expr::ensure_where(#op), ident.clone())
);
list.push(expr);
};
if is_option {
quote! {
if let Some(ident) = &self.#ident {
#push_quote
}
}
} else {
quote! {
{
let ident = &self.#ident;
#push_quote;
}
}
}
});
quote! {
impl<DB> #crate_name::builder::PushToBuilder<DB> for #struct_name where DB: ::sqlx::Database {
fn push_to(&self, builder: &mut ::sqlx::QueryBuilder<'_, DB>) {
let mut list = #crate_name::builder::ExprList::<DB, #crate_name::builder::expr::And>::new(::std::vec::Vec::new());
#(
#push_clauses
)*
#crate_name::builder::PushToBuilder::push_to(list, builder);
}
}
}
}
#[cfg(test)]
#[allow(clippy::print_stdout)]
mod test {
use crate::where_clause::where_clause;
#[test]
fn derive() {
let input = r#"
#[derive(WhereClause)]
struct NagelTest {
#[sqlxu(rename = "fusgesicht")]
name: String
}
"#;
let process = where_clause(input.parse().expect("Failed parse"));
println!("{process}");
}
}

View File

@@ -187,6 +187,24 @@ where
}
}
impl<DB> PushToBuilder<DB> for String
where
DB: Database,
{
fn push_to(&self, builder: &mut QueryBuilder<'_, DB>) {
builder.push(self);
}
}
impl<DB> PushToBuilder<DB> for &str
where
DB: Database,
{
fn push_to(&self, builder: &mut QueryBuilder<'_, DB>) {
builder.push(self);
}
}
impl<DB, T> PushToBuilder<DB> for Option<T>
where
T: PushToBuilder<DB>,
@@ -194,8 +212,12 @@ where
{
fn push_to(&self, builder: &mut QueryBuilder<'_, DB>) {
match self {
None => {}
Some(e) => e.push_to(builder),
None => {
builder.push("NULL");
}
Some(e) => {
e.push_to(builder);
}
}
}
}

View File

@@ -135,6 +135,10 @@ pub const fn get_where_expr<T: WhereOp>() -> &'static str {
<T as SqlOperand>::EXPR
}
pub const fn ensure_where<T: WhereOp>(t: T) -> T {
t
}
mod sealed {
use crate::builder::expr::{
And, Comma, Equals, Ilike, In, Is, IsNot, Like, NotEquals, NotIlike, NotIn, NotLike, Or,

View File

@@ -1 +1,3 @@
pub mod builder;
pub use sqlx_utils_macros::*;