mirror of
https://github.com/Phundrak/georm.git
synced 2026-07-29 04:19:18 +02:00
refactor(macros): introduce SqlDialect abstraction, route Postgres codegen through it
All CRUD and relationship query builders duplicated the same $N placeholder formatting and Postgres-specific trait bounds/row type across traits/*.rs, ir/*.rs, and mod.rs. Consolidate that into a single SqlDialect trait with PostgresDialect as the only implementation for now, so a SQLite dialect can be added later without touching every query builder. Behavior-preserving: full test suite passes unchanged against a live Postgres database.
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::georm::sql::{self, FetchKind, SqlDialect};
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
#[derive(deluxe::ParseMetaItem, Clone)]
|
#[derive(deluxe::ParseMetaItem, Clone)]
|
||||||
@@ -60,7 +61,7 @@ impl From<&M2MRelationshipComplete> for proc_macro2::TokenStream {
|
|||||||
FROM {} local
|
FROM {} local
|
||||||
JOIN {} link ON link.{} = local.{}
|
JOIN {} link ON link.{} = local.{}
|
||||||
JOIN {} remote ON link.{} = remote.{}
|
JOIN {} remote ON link.{} = remote.{}
|
||||||
WHERE local.{} = $1",
|
WHERE local.{} = {}",
|
||||||
value.local.table,
|
value.local.table,
|
||||||
value.link.table,
|
value.link.table,
|
||||||
value.link.from,
|
value.link.from,
|
||||||
@@ -68,15 +69,15 @@ WHERE local.{} = $1",
|
|||||||
value.remote.table,
|
value.remote.table,
|
||||||
value.link.to,
|
value.link.to,
|
||||||
value.remote.id,
|
value.remote.id,
|
||||||
value.local.id
|
value.local.id,
|
||||||
|
sql::DIALECT.placeholder(1)
|
||||||
);
|
);
|
||||||
quote! {
|
sql::DIALECT.generate_relation_lookup(
|
||||||
pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Vec<#entity>>
|
&function,
|
||||||
where
|
entity,
|
||||||
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
|
&query,
|
||||||
{
|
"e! { self.get_id() },
|
||||||
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(executor).await
|
&FetchKind::Many,
|
||||||
}
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::georm::sql::SqlDialect;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
pub mod simple_relationship;
|
pub mod simple_relationship;
|
||||||
@@ -156,28 +157,24 @@ impl From<&GeormField> for proc_macro2::TokenStream {
|
|||||||
proc_macro2::Span::call_site(),
|
proc_macro2::Span::call_site(),
|
||||||
);
|
);
|
||||||
let entity = &relation.entity;
|
let entity = &relation.entity;
|
||||||
let return_type = if relation.nullable {
|
|
||||||
quote! { Option<#entity> }
|
|
||||||
} else {
|
|
||||||
quote! { #entity }
|
|
||||||
};
|
|
||||||
let query = format!(
|
let query = format!(
|
||||||
"SELECT * FROM {} WHERE {} = $1",
|
"SELECT * FROM {} WHERE {} = {}",
|
||||||
relation.table, relation.remote_id
|
relation.table,
|
||||||
|
relation.remote_id,
|
||||||
|
crate::georm::sql::DIALECT.placeholder(1)
|
||||||
);
|
);
|
||||||
let local_ident = &value.field.ident;
|
let local_ident = &value.field.ident;
|
||||||
let fetch = if relation.nullable {
|
let fetch = if relation.nullable {
|
||||||
quote! { fetch_optional }
|
crate::georm::sql::FetchKind::Optional
|
||||||
} else {
|
} else {
|
||||||
quote! { fetch_one }
|
crate::georm::sql::FetchKind::One
|
||||||
};
|
};
|
||||||
quote! {
|
crate::georm::sql::DIALECT.generate_relation_lookup(
|
||||||
pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result<#return_type>
|
&function,
|
||||||
where
|
entity,
|
||||||
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
|
&query,
|
||||||
{
|
"e! { self.#local_ident },
|
||||||
::sqlx::query_as!(#entity, #query, self.#local_ident).#fetch(executor).await
|
&fetch,
|
||||||
}
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::georm::sql::{self, FetchKind, SqlDialect};
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
pub trait SimpleRelationshipType {}
|
pub trait SimpleRelationshipType {}
|
||||||
@@ -28,7 +29,12 @@ where
|
|||||||
T: SimpleRelationshipType + deluxe::ParseMetaItem + Default,
|
T: SimpleRelationshipType + deluxe::ParseMetaItem + Default,
|
||||||
{
|
{
|
||||||
pub fn make_query(&self) -> String {
|
pub fn make_query(&self) -> String {
|
||||||
format!("SELECT * FROM {} WHERE {} = $1", self.table, self.remote_id)
|
format!(
|
||||||
|
"SELECT * FROM {} WHERE {} = {}",
|
||||||
|
self.table,
|
||||||
|
self.remote_id,
|
||||||
|
sql::DIALECT.placeholder(1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_function_name(&self) -> syn::Ident {
|
pub fn make_function_name(&self) -> syn::Ident {
|
||||||
@@ -44,14 +50,13 @@ impl From<&SimpleRelationship<OneToOne>> for proc_macro2::TokenStream {
|
|||||||
let query = value.make_query();
|
let query = value.make_query();
|
||||||
let entity = &value.entity;
|
let entity = &value.entity;
|
||||||
let function = value.make_function_name();
|
let function = value.make_function_name();
|
||||||
quote! {
|
sql::DIALECT.generate_relation_lookup(
|
||||||
pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Option<#entity>>
|
&function,
|
||||||
where
|
entity,
|
||||||
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
|
&query,
|
||||||
{
|
"e! { self.get_id() },
|
||||||
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_optional(executor).await
|
&FetchKind::Optional,
|
||||||
}
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,13 +65,12 @@ impl From<&SimpleRelationship<OneToMany>> for proc_macro2::TokenStream {
|
|||||||
let query = value.make_query();
|
let query = value.make_query();
|
||||||
let entity = &value.entity;
|
let entity = &value.entity;
|
||||||
let function = value.make_function_name();
|
let function = value.make_function_name();
|
||||||
quote! {
|
sql::DIALECT.generate_relation_lookup(
|
||||||
pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Vec<#entity>>
|
&function,
|
||||||
where
|
entity,
|
||||||
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
|
&query,
|
||||||
{
|
"e! { self.get_id() },
|
||||||
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(executor).await
|
&FetchKind::Many,
|
||||||
}
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ mod defaultable_struct;
|
|||||||
mod ir;
|
mod ir;
|
||||||
pub(crate) use ir::GeormField;
|
pub(crate) use ir::GeormField;
|
||||||
mod relationships;
|
mod relationships;
|
||||||
|
mod sql;
|
||||||
mod traits;
|
mod traits;
|
||||||
pub(crate) use composite_keys::IdType;
|
pub(crate) use composite_keys::IdType;
|
||||||
|
|
||||||
@@ -67,18 +68,6 @@ fn generate_from_row_impl(
|
|||||||
ast: &syn::DeriveInput,
|
ast: &syn::DeriveInput,
|
||||||
fields: &[GeormField],
|
fields: &[GeormField],
|
||||||
) -> proc_macro2::TokenStream {
|
) -> proc_macro2::TokenStream {
|
||||||
let struct_name = &ast.ident;
|
use sql::SqlDialect;
|
||||||
let field_idents: Vec<&syn::Ident> = fields.iter().map(|f| &f.ident).collect();
|
sql::DIALECT.generate_from_row(&ast.ident, fields)
|
||||||
let field_names: Vec<String> = fields.iter().map(|f| f.ident.to_string()).collect();
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
impl<'r> ::sqlx::FromRow<'r, ::sqlx::postgres::PgRow> for #struct_name {
|
|
||||||
fn from_row(row: &'r ::sqlx::postgres::PgRow) -> ::sqlx::Result<Self> {
|
|
||||||
use ::sqlx::Row;
|
|
||||||
Ok(Self {
|
|
||||||
#(#field_idents: row.try_get(#field_names)?),*
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,332 @@
|
|||||||
|
use crate::georm::composite_keys::IdType;
|
||||||
|
use crate::georm::ir::GeneratedType;
|
||||||
|
use crate::georm::ir::GeormField;
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
mod postgres;
|
||||||
|
pub use postgres::PostgresDialect;
|
||||||
|
|
||||||
|
pub type ActiveDialect = PostgresDialect;
|
||||||
|
pub const DIALECT: ActiveDialect = PostgresDialect;
|
||||||
|
|
||||||
|
/// How a relation-lookup query should fetch its result.
|
||||||
|
pub enum FetchKind {
|
||||||
|
/// `fetch_one`, returns `Entity`
|
||||||
|
One,
|
||||||
|
/// `fetch_optional`, returns `Option<Entity>`
|
||||||
|
Optional,
|
||||||
|
/// `fetch_all`, returns `Vec<Entity>`
|
||||||
|
Many,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Abstracts the SQL dialect-specific parts of Georm's generated code: bind
|
||||||
|
/// parameter placeholder syntax, and the `sqlx` database/row types used in
|
||||||
|
/// generated trait bounds and `FromRow` impls. `RETURNING` and
|
||||||
|
/// `ON CONFLICT ... DO UPDATE` are supported identically by every dialect
|
||||||
|
/// Georm targets, so query shape itself does not need to vary.
|
||||||
|
pub trait SqlDialect {
|
||||||
|
fn placeholder(&self, index: usize) -> String;
|
||||||
|
fn database_type(&self) -> proc_macro2::TokenStream;
|
||||||
|
fn row_type(&self) -> proc_macro2::TokenStream;
|
||||||
|
|
||||||
|
fn generate_from_row(
|
||||||
|
&self,
|
||||||
|
struct_name: &syn::Ident,
|
||||||
|
fields: &[GeormField],
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
let field_idents: Vec<&syn::Ident> = fields.iter().map(|f| &f.ident).collect();
|
||||||
|
let field_names: Vec<String> = fields.iter().map(|f| f.ident.to_string()).collect();
|
||||||
|
let row = self.row_type();
|
||||||
|
quote! {
|
||||||
|
impl<'r> ::sqlx::FromRow<'r, #row> for #struct_name {
|
||||||
|
fn from_row(row: &'r #row) -> ::sqlx::Result<Self> {
|
||||||
|
use ::sqlx::Row;
|
||||||
|
Ok(Self {
|
||||||
|
#(#field_idents: row.try_get(#field_names)?),*
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_find_all(&self, table: &str) -> proc_macro2::TokenStream {
|
||||||
|
let find_string = format!("SELECT * FROM {table}");
|
||||||
|
let database = self.database_type();
|
||||||
|
quote! {
|
||||||
|
async fn find_all<'e, E>(mut executor: E) -> ::sqlx::Result<Vec<Self>>
|
||||||
|
where
|
||||||
|
E: ::sqlx::Executor<'e, Database = #database>
|
||||||
|
{
|
||||||
|
::sqlx::query_as!(Self, #find_string).fetch_all(executor).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_find(&self, table: &str, id: &IdType) -> proc_macro2::TokenStream {
|
||||||
|
let database = self.database_type();
|
||||||
|
match id {
|
||||||
|
IdType::Simple {
|
||||||
|
field_name,
|
||||||
|
field_type,
|
||||||
|
} => {
|
||||||
|
let placeholder = self.placeholder(1);
|
||||||
|
let find_string =
|
||||||
|
format!("SELECT * FROM {table} WHERE {field_name} = {placeholder}");
|
||||||
|
quote! {
|
||||||
|
async fn find<'e, E>(mut executor: E, id: &#field_type) -> ::sqlx::Result<Option<Self>>
|
||||||
|
where
|
||||||
|
E: ::sqlx::Executor<'e, Database = #database>
|
||||||
|
{
|
||||||
|
::sqlx::query_as!(Self, #find_string, id)
|
||||||
|
.fetch_optional(executor)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IdType::Composite { fields, field_type } => {
|
||||||
|
let id_match_string = fields
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, field)| format!("{} = {}", field.name, self.placeholder(i + 1)))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" AND ");
|
||||||
|
let id_members: Vec<syn::Ident> =
|
||||||
|
fields.iter().map(|field| field.name.clone()).collect();
|
||||||
|
let find_string = format!("SELECT * FROM {table} WHERE {id_match_string}");
|
||||||
|
quote! {
|
||||||
|
async fn find<'e, E>(mut executor: E, id: &#field_type) -> ::sqlx::Result<Option<Self>>
|
||||||
|
where
|
||||||
|
E: ::sqlx::Executor<'e, Database = #database>
|
||||||
|
{
|
||||||
|
::sqlx::query_as!(Self, #find_string, #(id.#id_members),*)
|
||||||
|
.fetch_optional(executor)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_create(&self, table: &str, fields: &[GeormField]) -> proc_macro2::TokenStream {
|
||||||
|
let insert_fields: Vec<&GeormField> = fields
|
||||||
|
.iter()
|
||||||
|
.filter(|field| !field.exclude_from_insert())
|
||||||
|
.collect();
|
||||||
|
let field_names: Vec<String> = insert_fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| field.ident.to_string())
|
||||||
|
.collect();
|
||||||
|
let field_idents: Vec<syn::Ident> = insert_fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| field.ident.clone())
|
||||||
|
.collect();
|
||||||
|
let placeholders: Vec<String> = (1..=insert_fields.len())
|
||||||
|
.map(|i| self.placeholder(i))
|
||||||
|
.collect();
|
||||||
|
let query = format!(
|
||||||
|
"INSERT INTO {table} ({}) VALUES ({}) RETURNING *",
|
||||||
|
field_names.join(", "),
|
||||||
|
placeholders.join(", ")
|
||||||
|
);
|
||||||
|
let database = self.database_type();
|
||||||
|
quote! {
|
||||||
|
async fn create<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Self>
|
||||||
|
where
|
||||||
|
E: ::sqlx::Executor<'e, Database = #database>
|
||||||
|
{
|
||||||
|
::sqlx::query_as!(
|
||||||
|
Self,
|
||||||
|
#query,
|
||||||
|
#(self.#field_idents),*
|
||||||
|
)
|
||||||
|
.fetch_one(executor)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_update(&self, table: &str, fields: &[GeormField]) -> proc_macro2::TokenStream {
|
||||||
|
let update_fields: Vec<&GeormField> = fields
|
||||||
|
.iter()
|
||||||
|
.filter(|field| !field.is_id && !field.exclude_from_update())
|
||||||
|
.collect();
|
||||||
|
let update_idents: Vec<syn::Ident> = update_fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| field.ident.clone())
|
||||||
|
.collect();
|
||||||
|
let id_fields: Vec<&GeormField> = fields.iter().filter(|field| field.is_id).collect();
|
||||||
|
let id_idents: Vec<syn::Ident> = id_fields.iter().map(|f| f.ident.clone()).collect();
|
||||||
|
let set_clauses: Vec<String> = update_fields
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, field)| format!("{} = {}", field.ident, self.placeholder(i + 1)))
|
||||||
|
.collect();
|
||||||
|
let where_clauses: Vec<String> = id_fields
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, field)| {
|
||||||
|
format!(
|
||||||
|
"{} = {}",
|
||||||
|
field.ident,
|
||||||
|
self.placeholder(update_fields.len() + i + 1)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let query = format!(
|
||||||
|
"UPDATE {table} SET {} WHERE {} RETURNING *",
|
||||||
|
set_clauses.join(", "),
|
||||||
|
where_clauses.join(" AND ")
|
||||||
|
);
|
||||||
|
let database = self.database_type();
|
||||||
|
quote! {
|
||||||
|
async fn update<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Self>
|
||||||
|
where
|
||||||
|
E: ::sqlx::Executor<'e, Database = #database>
|
||||||
|
{
|
||||||
|
::sqlx::query_as!(
|
||||||
|
Self,
|
||||||
|
#query,
|
||||||
|
#(self.#update_idents),*,
|
||||||
|
#(self.#id_idents),*
|
||||||
|
)
|
||||||
|
.fetch_one(executor)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_upsert(
|
||||||
|
&self,
|
||||||
|
table: &str,
|
||||||
|
fields: &[GeormField],
|
||||||
|
id: &IdType,
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
let fields: Vec<&GeormField> = fields
|
||||||
|
.iter()
|
||||||
|
.filter(|field| !matches!(field.generated_type, GeneratedType::Always))
|
||||||
|
.collect();
|
||||||
|
let inputs: Vec<String> = (1..=fields.len()).map(|i| self.placeholder(i)).collect();
|
||||||
|
let columns = fields
|
||||||
|
.iter()
|
||||||
|
.map(|f| f.ident.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
let primary_key: proc_macro2::TokenStream = match id {
|
||||||
|
IdType::Simple { field_name, .. } => quote! {#field_name},
|
||||||
|
IdType::Composite { fields, .. } => {
|
||||||
|
let field_names: Vec<syn::Ident> = fields.iter().map(|f| f.name.clone()).collect();
|
||||||
|
quote! {
|
||||||
|
#(#field_names),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// For ON CONFLICT DO UPDATE, exclude the ID field from updates
|
||||||
|
let update_assignments = fields
|
||||||
|
.iter()
|
||||||
|
.filter(|f| !f.is_id)
|
||||||
|
.map(|f| format!("{} = EXCLUDED.{}", f.ident, f.ident))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
let upsert_string = format!(
|
||||||
|
"INSERT INTO {table} ({columns}) VALUES ({}) ON CONFLICT ({}) DO UPDATE SET {update_assignments} RETURNING *",
|
||||||
|
inputs.join(", "),
|
||||||
|
primary_key
|
||||||
|
);
|
||||||
|
|
||||||
|
let field_idents: Vec<syn::Ident> = fields.iter().map(|f| f.ident.clone()).collect();
|
||||||
|
let database = self.database_type();
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
async fn upsert<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Self>
|
||||||
|
where
|
||||||
|
E: ::sqlx::Executor<'e, Database = #database>
|
||||||
|
{
|
||||||
|
::sqlx::query_as!(
|
||||||
|
Self,
|
||||||
|
#upsert_string,
|
||||||
|
#(self.#field_idents),*
|
||||||
|
)
|
||||||
|
.fetch_one(executor)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_delete(&self, table: &str, id: &IdType) -> proc_macro2::TokenStream {
|
||||||
|
let where_clause = match id {
|
||||||
|
IdType::Simple { field_name, .. } => {
|
||||||
|
format!("{} = {}", field_name, self.placeholder(1))
|
||||||
|
}
|
||||||
|
IdType::Composite { fields, .. } => fields
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, field)| format!("{} = {}", field.name, self.placeholder(i + 1)))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" AND "),
|
||||||
|
};
|
||||||
|
let query_args = match id {
|
||||||
|
IdType::Simple { .. } => quote! { id },
|
||||||
|
IdType::Composite { fields, .. } => {
|
||||||
|
let fields: Vec<syn::Ident> = fields.iter().map(|f| f.name.clone()).collect();
|
||||||
|
quote! { #(id.#fields), * }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let id_type = match id {
|
||||||
|
IdType::Simple { field_type, .. } => quote! { #field_type },
|
||||||
|
IdType::Composite { field_type, .. } => quote! { #field_type },
|
||||||
|
};
|
||||||
|
let delete_string = format!("DELETE FROM {table} WHERE {where_clause}");
|
||||||
|
let database = self.database_type();
|
||||||
|
quote! {
|
||||||
|
async fn delete<'e, E>(&self, mut executor: E) -> ::sqlx::Result<u64>
|
||||||
|
where
|
||||||
|
E: ::sqlx::Executor<'e, Database = #database>
|
||||||
|
{
|
||||||
|
Self::delete_by_id(executor, &self.get_id()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_by_id<'e, E>(mut executor: E, id: &#id_type) -> ::sqlx::Result<u64>
|
||||||
|
where
|
||||||
|
E: ::sqlx::Executor<'e, Database = #database>
|
||||||
|
{
|
||||||
|
let rows_affected = ::sqlx::query!(#delete_string, #query_args)
|
||||||
|
.execute(executor)
|
||||||
|
.await?
|
||||||
|
.rows_affected();
|
||||||
|
Ok(rows_affected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps a single-parameter relation-lookup query (field-level
|
||||||
|
/// `#[georm(relation = ...)]`, struct-level `one_to_one`/`one_to_many`,
|
||||||
|
/// and many-to-many joins) in an async method with the right executor
|
||||||
|
/// bound. `query` must already contain this dialect's placeholder syntax
|
||||||
|
/// (built via [`SqlDialect::placeholder`]).
|
||||||
|
fn generate_relation_lookup(
|
||||||
|
&self,
|
||||||
|
function: &syn::Ident,
|
||||||
|
entity: &syn::Type,
|
||||||
|
query: &str,
|
||||||
|
arg: &proc_macro2::TokenStream,
|
||||||
|
fetch: &FetchKind,
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
let database = self.database_type();
|
||||||
|
let (return_type, fetch_method) = match fetch {
|
||||||
|
FetchKind::One => (quote! { #entity }, quote! { fetch_one }),
|
||||||
|
FetchKind::Optional => (quote! { Option<#entity> }, quote! { fetch_optional }),
|
||||||
|
FetchKind::Many => (quote! { Vec<#entity> }, quote! { fetch_all }),
|
||||||
|
};
|
||||||
|
quote! {
|
||||||
|
pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result<#return_type>
|
||||||
|
where
|
||||||
|
E: ::sqlx::Executor<'e, Database = #database>
|
||||||
|
{
|
||||||
|
::sqlx::query_as!(#entity, #query, #arg).#fetch_method(executor).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
use super::SqlDialect;
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
pub struct PostgresDialect;
|
||||||
|
|
||||||
|
impl SqlDialect for PostgresDialect {
|
||||||
|
fn placeholder(&self, index: usize) -> String {
|
||||||
|
format!("${index}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn database_type(&self) -> proc_macro2::TokenStream {
|
||||||
|
quote! { ::sqlx::Postgres }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row_type(&self) -> proc_macro2::TokenStream {
|
||||||
|
quote! { ::sqlx::postgres::PgRow }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,6 @@
|
|||||||
use crate::georm::GeormField;
|
use crate::georm::GeormField;
|
||||||
use quote::quote;
|
use crate::georm::sql::{self, SqlDialect};
|
||||||
|
|
||||||
pub fn generate_create_query(table_name: &str, fields: &[GeormField]) -> proc_macro2::TokenStream {
|
pub fn generate_create_query(table_name: &str, fields: &[GeormField]) -> proc_macro2::TokenStream {
|
||||||
let insert_fields: Vec<&GeormField> = fields
|
sql::DIALECT.generate_create(table_name, fields)
|
||||||
.iter()
|
|
||||||
.filter(|field| !field.exclude_from_insert())
|
|
||||||
.collect();
|
|
||||||
let field_names: Vec<String> = insert_fields
|
|
||||||
.iter()
|
|
||||||
.map(|field| field.ident.to_string())
|
|
||||||
.collect();
|
|
||||||
let field_idents: Vec<syn::Ident> = insert_fields
|
|
||||||
.iter()
|
|
||||||
.map(|field| field.ident.clone())
|
|
||||||
.collect();
|
|
||||||
let placeholders: Vec<String> = (1..=insert_fields.len()).map(|i| format!("${i}")).collect();
|
|
||||||
let query = format!(
|
|
||||||
"INSERT INTO {table_name} ({}) VALUES ({}) RETURNING *",
|
|
||||||
field_names.join(", "),
|
|
||||||
placeholders.join(", ")
|
|
||||||
);
|
|
||||||
quote! {
|
|
||||||
async fn create<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Self>
|
|
||||||
where
|
|
||||||
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
|
|
||||||
{
|
|
||||||
::sqlx::query_as!(
|
|
||||||
Self,
|
|
||||||
#query,
|
|
||||||
#(self.#field_idents),*
|
|
||||||
)
|
|
||||||
.fetch_one(executor)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,6 @@
|
|||||||
use crate::georm::IdType;
|
use crate::georm::IdType;
|
||||||
use quote::quote;
|
use crate::georm::sql::{self, SqlDialect};
|
||||||
|
|
||||||
pub fn generate_delete_query(table: &str, id: &IdType) -> proc_macro2::TokenStream {
|
pub fn generate_delete_query(table: &str, id: &IdType) -> proc_macro2::TokenStream {
|
||||||
let where_clause = match id {
|
sql::DIALECT.generate_delete(table, id)
|
||||||
IdType::Simple { field_name, .. } => format!("{} = $1", field_name),
|
|
||||||
IdType::Composite { fields, .. } => fields
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, field)| format!("{} = ${}", field.name, i + 1))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(" AND "),
|
|
||||||
};
|
|
||||||
let query_args = match id {
|
|
||||||
IdType::Simple { .. } => quote! { id },
|
|
||||||
IdType::Composite { fields, .. } => {
|
|
||||||
let fields: Vec<syn::Ident> = fields.iter().map(|f| f.name.clone()).collect();
|
|
||||||
quote! { #(id.#fields), * }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let id_type = match id {
|
|
||||||
IdType::Simple { field_type, .. } => quote! { #field_type },
|
|
||||||
IdType::Composite { field_type, .. } => quote! { #field_type },
|
|
||||||
};
|
|
||||||
let delete_string = format!("DELETE FROM {table} WHERE {where_clause}");
|
|
||||||
quote! {
|
|
||||||
async fn delete<'e, E>(&self, mut executor: E) -> ::sqlx::Result<u64>
|
|
||||||
where
|
|
||||||
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
|
|
||||||
{
|
|
||||||
Self::delete_by_id(executor, &self.get_id()).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_by_id<'e, E>(mut executor: E, id: &#id_type) -> ::sqlx::Result<u64>
|
|
||||||
where
|
|
||||||
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
|
|
||||||
{
|
|
||||||
let rows_affected = ::sqlx::query!(#delete_string, #query_args)
|
|
||||||
.execute(executor)
|
|
||||||
.await?
|
|
||||||
.rows_affected();
|
|
||||||
Ok(rows_affected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,10 @@
|
|||||||
use crate::georm::IdType;
|
use crate::georm::IdType;
|
||||||
use quote::quote;
|
use crate::georm::sql::{self, SqlDialect};
|
||||||
|
|
||||||
pub fn generate_find_all_query(table: &str) -> proc_macro2::TokenStream {
|
pub fn generate_find_all_query(table: &str) -> proc_macro2::TokenStream {
|
||||||
let find_string = format!("SELECT * FROM {table}");
|
sql::DIALECT.generate_find_all(table)
|
||||||
quote! {
|
|
||||||
async fn find_all<'e, E>(mut executor: E) -> ::sqlx::Result<Vec<Self>>
|
|
||||||
where
|
|
||||||
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
|
|
||||||
{
|
|
||||||
::sqlx::query_as!(Self, #find_string).fetch_all(executor).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_find_query(table: &str, id: &IdType) -> proc_macro2::TokenStream {
|
pub fn generate_find_query(table: &str, id: &IdType) -> proc_macro2::TokenStream {
|
||||||
match id {
|
sql::DIALECT.generate_find(table, id)
|
||||||
IdType::Simple {
|
|
||||||
field_name,
|
|
||||||
field_type,
|
|
||||||
} => {
|
|
||||||
let find_string = format!("SELECT * FROM {table} WHERE {} = $1", field_name);
|
|
||||||
quote! {
|
|
||||||
async fn find<'e, E>(mut executor: E, id: &#field_type) -> ::sqlx::Result<Option<Self>>
|
|
||||||
where
|
|
||||||
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
|
|
||||||
{
|
|
||||||
::sqlx::query_as!(Self, #find_string, id)
|
|
||||||
.fetch_optional(executor)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IdType::Composite { fields, field_type } => {
|
|
||||||
let id_match_string = fields
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, field)| format!("{} = ${}", field.name, i + 1))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(" AND ");
|
|
||||||
let id_members: Vec<syn::Ident> =
|
|
||||||
fields.iter().map(|field| field.name.clone()).collect();
|
|
||||||
let find_string = format!("SELECT * FROM {table} WHERE {id_match_string}");
|
|
||||||
quote! {
|
|
||||||
async fn find<'e, E>(mut executor: E, id: &#field_type) -> ::sqlx::Result<Option<Self>>
|
|
||||||
where
|
|
||||||
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
|
|
||||||
{
|
|
||||||
::sqlx::query_as!(Self, #find_string, #(id.#id_members),*)
|
|
||||||
.fetch_optional(executor)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,6 @@
|
|||||||
use crate::georm::GeormField;
|
use crate::georm::GeormField;
|
||||||
use quote::quote;
|
use crate::georm::sql::{self, SqlDialect};
|
||||||
|
|
||||||
pub fn generate_update_query(table_name: &str, fields: &[GeormField]) -> proc_macro2::TokenStream {
|
pub fn generate_update_query(table_name: &str, fields: &[GeormField]) -> proc_macro2::TokenStream {
|
||||||
let update_fields: Vec<&GeormField> = fields
|
sql::DIALECT.generate_update(table_name, fields)
|
||||||
.iter()
|
|
||||||
.filter(|field| !field.is_id && !field.exclude_from_update())
|
|
||||||
.collect();
|
|
||||||
let update_idents: Vec<syn::Ident> = update_fields
|
|
||||||
.iter()
|
|
||||||
.map(|field| field.ident.clone())
|
|
||||||
.collect();
|
|
||||||
let id_fields: Vec<&GeormField> = fields.iter().filter(|field| field.is_id).collect();
|
|
||||||
let id_idents: Vec<syn::Ident> = id_fields.iter().map(|f| f.ident.clone()).collect();
|
|
||||||
let set_clauses: Vec<String> = update_fields
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, field)| format!("{} = ${}", field.ident, i + 1))
|
|
||||||
.collect();
|
|
||||||
let where_clauses: Vec<String> = id_fields
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, field)| format!("{} = ${}", field.ident, update_fields.len() + i + 1))
|
|
||||||
.collect();
|
|
||||||
let query = format!(
|
|
||||||
"UPDATE {table_name} SET {} WHERE {} RETURNING *",
|
|
||||||
set_clauses.join(", "),
|
|
||||||
where_clauses.join(" AND ")
|
|
||||||
);
|
|
||||||
quote! {
|
|
||||||
async fn update<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Self>
|
|
||||||
where
|
|
||||||
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
|
|
||||||
{
|
|
||||||
::sqlx::query_as!(
|
|
||||||
Self,
|
|
||||||
#query,
|
|
||||||
#(self.#update_idents),*,
|
|
||||||
#(self.#id_idents),*
|
|
||||||
)
|
|
||||||
.fetch_one(executor)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,10 @@
|
|||||||
use crate::georm::{GeormField, IdType, ir::GeneratedType};
|
use crate::georm::sql::{self, SqlDialect};
|
||||||
use quote::quote;
|
use crate::georm::{GeormField, IdType};
|
||||||
|
|
||||||
pub fn generate_upsert_query(
|
pub fn generate_upsert_query(
|
||||||
table: &str,
|
table: &str,
|
||||||
fields: &[GeormField],
|
fields: &[GeormField],
|
||||||
id: &IdType,
|
id: &IdType,
|
||||||
) -> proc_macro2::TokenStream {
|
) -> proc_macro2::TokenStream {
|
||||||
let fields: Vec<&GeormField> = fields
|
sql::DIALECT.generate_upsert(table, fields, id)
|
||||||
.iter()
|
|
||||||
.filter(|field| !matches!(field.generated_type, GeneratedType::Always))
|
|
||||||
.collect();
|
|
||||||
let inputs: Vec<String> = (1..=fields.len()).map(|num| format!("${num}")).collect();
|
|
||||||
let columns = fields
|
|
||||||
.iter()
|
|
||||||
.map(|f| f.ident.to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
let primary_key: proc_macro2::TokenStream = match id {
|
|
||||||
IdType::Simple { field_name, .. } => quote! {#field_name},
|
|
||||||
IdType::Composite { fields, .. } => {
|
|
||||||
let field_names: Vec<syn::Ident> = fields.iter().map(|f| f.name.clone()).collect();
|
|
||||||
quote! {
|
|
||||||
#(#field_names),*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// For ON CONFLICT DO UPDATE, exclude the ID field from updates
|
|
||||||
let update_assignments = fields
|
|
||||||
.iter()
|
|
||||||
.filter(|f| !f.is_id)
|
|
||||||
.map(|f| format!("{} = EXCLUDED.{}", f.ident, f.ident))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
let upsert_string = format!(
|
|
||||||
"INSERT INTO {table} ({columns}) VALUES ({}) ON CONFLICT ({}) DO UPDATE SET {update_assignments} RETURNING *",
|
|
||||||
inputs.join(", "),
|
|
||||||
primary_key
|
|
||||||
);
|
|
||||||
|
|
||||||
let field_idents: Vec<syn::Ident> = fields.iter().map(|f| f.ident.clone()).collect();
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
async fn upsert<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Self>
|
|
||||||
where
|
|
||||||
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
|
|
||||||
{
|
|
||||||
::sqlx::query_as!(
|
|
||||||
Self,
|
|
||||||
#upsert_string,
|
|
||||||
#(self.#field_idents),*
|
|
||||||
)
|
|
||||||
.fetch_one(executor)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user