From 138635615e063c591d93909b1a35ef9dedbef966 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 04:33:49 +0200 Subject: [PATCH 01/19] get stuff working with 0.8 --- example-mysql/Cargo.toml | 8 ++++---- example-mysql/src/main.rs | 7 ++++--- example-postgres/Cargo.toml | 8 ++++---- example-postgres/src/main.rs | 9 +++++---- ormx-macros/Cargo.toml | 6 +++--- ormx-macros/src/attrs.rs | 8 ++++---- ormx-macros/src/table/mod.rs | 4 ++-- ormx-macros/src/table/parse.rs | 2 +- ormx/Cargo.toml | 6 +++--- 9 files changed, 30 insertions(+), 28 deletions(-) diff --git a/example-mysql/Cargo.toml b/example-mysql/Cargo.toml index d7f74f3..fac0bdf 100644 --- a/example-mysql/Cargo.toml +++ b/example-mysql/Cargo.toml @@ -6,14 +6,14 @@ edition = "2018" [dependencies] ormx = { path = "../ormx", features = ["mysql"] } -tokio = { version = "1.1", features = ["full"] } +tokio = { version = "1", features = ["full"] } anyhow = "1" dotenv = "0.15" chrono = "0.4" -simple_logger = "1" +env_logger = "0.11.5" log = "0.4" [dependencies.sqlx] -version = "0.6" +version = "0.8" default-features = false -features = ["macros", "mysql", "runtime-tokio-rustls", "chrono", "offline"] +features = ["macros", "mysql", "runtime-tokio-rustls", "chrono"] diff --git a/example-mysql/src/main.rs b/example-mysql/src/main.rs index d8ee7f3..da7a13e 100644 --- a/example-mysql/src/main.rs +++ b/example-mysql/src/main.rs @@ -1,5 +1,6 @@ // #![feature(trace_macros)] use chrono::{NaiveDateTime, Utc}; +use log::LevelFilter; use ormx::{Insert, Table}; use sqlx::MySqlPool; @@ -13,9 +14,9 @@ mod query2; #[tokio::main] async fn main() -> anyhow::Result<()> { dotenv::dotenv().ok(); - simple_logger::SimpleLogger::new() - .with_level(log::LevelFilter::Info) - .init()?; + env_logger::builder() + .filter_level(LevelFilter::Debug) + .init(); let db = MySqlPool::connect(&dotenv::var("DATABASE_URL")?).await?; diff --git a/example-postgres/Cargo.toml b/example-postgres/Cargo.toml index f6b1f48..d4b72ea 100644 --- a/example-postgres/Cargo.toml +++ b/example-postgres/Cargo.toml @@ -6,14 +6,14 @@ edition = "2018" [dependencies] ormx = { path = "../ormx", features = ["postgres"] } -tokio = { version = "1.1", features = ["full"] } +tokio = { version = "1", features = ["full"] } anyhow = "1" dotenv = "0.15" chrono = "0.4" -simple_logger = "1" +env_logger = "0.11.5" log = "0.4" [dependencies.sqlx] -version = "0.6" +version = "0.8" default-features = false -features = ["macros", "postgres", "runtime-tokio-rustls", "chrono", "offline"] +features = ["macros", "postgres", "runtime-tokio-rustls", "chrono"] diff --git a/example-postgres/src/main.rs b/example-postgres/src/main.rs index 7309b64..25b51a0 100644 --- a/example-postgres/src/main.rs +++ b/example-postgres/src/main.rs @@ -1,6 +1,7 @@ // #![feature(trace_macros)] use chrono::{NaiveDateTime, Utc}; -use ormx::{Insert, Table, Delete}; +use log::LevelFilter; +use ormx::{Delete, Insert, Table}; use sqlx::PgPool; // trace_macros!(true); @@ -13,9 +14,9 @@ mod query2; #[tokio::main] async fn main() -> anyhow::Result<()> { dotenv::dotenv().ok(); - simple_logger::SimpleLogger::new() - .with_level(log::LevelFilter::Info) - .init()?; + env_logger::builder() + .filter_level(LevelFilter::Debug) + .init(); let db = PgPool::connect(&dotenv::var("DATABASE_URL")?).await?; diff --git a/ormx-macros/Cargo.toml b/ormx-macros/Cargo.toml index ac08ffa..70b2f25 100644 --- a/ormx-macros/Cargo.toml +++ b/ormx-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ormx-macros" -version = "0.10.4" +version = "0.11.0" authors = ["moritz"] edition = "2018" description = "lightweight procedural macros bringing orm-like features to sqlx" @@ -16,9 +16,9 @@ mysql = [] postgres = [] [dependencies] -itertools = "0.10" +itertools = "0.13.0" proc-macro2 = "1" quote = "1" -syn = { version = "1", features = ["full"] } +syn = { version = "2", features = ["full"] } once_cell = "1" proc-macro-error = "1" diff --git a/ormx-macros/src/attrs.rs b/ormx-macros/src/attrs.rs index fc99434..bab82f2 100644 --- a/ormx-macros/src/attrs.rs +++ b/ormx-macros/src/attrs.rs @@ -12,7 +12,7 @@ pub enum TableAttr { // insertable [= []* ]? Insertable(Option), // deletable - Deletable(()) + Deletable(()), } pub struct Insertable { @@ -38,7 +38,7 @@ pub enum TableFieldAttr { // by_ref ByRef(()), // insert_attribute = - InsertAttr(AnyAttribute) + InsertAttr(AnyAttribute), } #[derive(Clone)] @@ -92,7 +92,7 @@ impl Parse for Insertable { pub fn parse_attrs(attrs: &[Attribute]) -> Result> { let attrs = attrs .iter() - .filter(|a| a.path.is_ident("ormx")) + .filter(|a| a.path().is_ident("ormx")) .map(|a| a.parse_args_with(Punctuated::::parse_terminated)) .collect::>>()? .into_iter() @@ -176,4 +176,4 @@ impl syn::parse::Parse for AnyAttribute { fn parse(input: ParseStream) -> Result { input.call(Attribute::parse_outer).map(Self) } -} \ No newline at end of file +} diff --git a/ormx-macros/src/table/mod.rs b/ormx-macros/src/table/mod.rs index 36d7864..05be44d 100644 --- a/ormx-macros/src/table/mod.rs +++ b/ormx-macros/src/table/mod.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, convert::TryFrom, marker::PhantomData}; use itertools::Itertools; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; -use syn::{DeriveInput, Result, Type, Visibility, Attribute}; +use syn::{Attribute, DeriveInput, Result, Type, Visibility}; use crate::{ attrs::{Getter, Insertable}, @@ -19,7 +19,7 @@ pub struct Table { pub id: TableField, pub fields: Vec>, pub insertable: Option, - pub deletable: bool + pub deletable: bool, } #[derive(Clone)] diff --git a/ormx-macros/src/table/parse.rs b/ormx-macros/src/table/parse.rs index 4842b28..008972c 100644 --- a/ormx-macros/src/table/parse.rs +++ b/ormx-macros/src/table/parse.rs @@ -131,7 +131,7 @@ impl TryFrom<&syn::DeriveInput> for Table { id, insertable, fields, - deletable: deletable.unwrap_or(false) + deletable: deletable.unwrap_or(false), }) } } diff --git a/ormx/Cargo.toml b/ormx/Cargo.toml index 5f6a93a..e551ccb 100644 --- a/ormx/Cargo.toml +++ b/ormx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ormx" -version = "0.10.0" +version = "0.11.0" authors = ["Moritz Bischof"] edition = "2018" license = "MIT" @@ -20,11 +20,11 @@ postgres = ["sqlx/postgres", "ormx-macros/postgres"] _docs-rs-build = ["sqlx/runtime-tokio-rustls", "postgres"] [dependencies] -ormx-macros = { path = "../ormx-macros", version = "0.10" } +ormx-macros = { path = "../ormx-macros", version = "=0.11.0" } futures = "0.3" [dependencies.sqlx] -version = "0.6" +version = "0.8" default-features = false features = ["macros"] From 2b8c13fb35f4994b0e961d8945a7c1948ef72ba2 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 04:35:07 +0200 Subject: [PATCH 02/19] 2021 edition --- Cargo.toml | 3 ++- example-mysql/Cargo.toml | 2 +- example-postgres/Cargo.toml | 2 +- ormx-macros/Cargo.toml | 2 +- ormx/Cargo.toml | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dd0abc7..e109007 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,3 @@ [workspace] -members = ["ormx-macros", "ormx", "example-postgres", "example-mysql"] \ No newline at end of file +members = ["ormx-macros", "ormx", "example-postgres", "example-mysql"] +resolver = "2" \ No newline at end of file diff --git a/example-mysql/Cargo.toml b/example-mysql/Cargo.toml index fac0bdf..ab8bf34 100644 --- a/example-mysql/Cargo.toml +++ b/example-mysql/Cargo.toml @@ -2,7 +2,7 @@ name = "example-mysql" version = "0.1.0" authors = ["moritz"] -edition = "2018" +edition = "2021" [dependencies] ormx = { path = "../ormx", features = ["mysql"] } diff --git a/example-postgres/Cargo.toml b/example-postgres/Cargo.toml index d4b72ea..fc3dd0f 100644 --- a/example-postgres/Cargo.toml +++ b/example-postgres/Cargo.toml @@ -2,7 +2,7 @@ name = "example-postgres" version = "0.1.0" authors = ["moritz"] -edition = "2018" +edition = "2021" [dependencies] ormx = { path = "../ormx", features = ["postgres"] } diff --git a/ormx-macros/Cargo.toml b/ormx-macros/Cargo.toml index 70b2f25..ff29fcd 100644 --- a/ormx-macros/Cargo.toml +++ b/ormx-macros/Cargo.toml @@ -2,7 +2,7 @@ name = "ormx-macros" version = "0.11.0" authors = ["moritz"] -edition = "2018" +edition = "2021" description = "lightweight procedural macros bringing orm-like features to sqlx" homepage = "https://github.com/NyxCode/ormx" license = "MIT" diff --git a/ormx/Cargo.toml b/ormx/Cargo.toml index e551ccb..dd25a89 100644 --- a/ormx/Cargo.toml +++ b/ormx/Cargo.toml @@ -2,7 +2,7 @@ name = "ormx" version = "0.11.0" authors = ["Moritz Bischof"] -edition = "2018" +edition = "2021" license = "MIT" description = "lightweight procedural macros bringing orm-like features to sqlx" homepage = "https://github.com/NyxCode/ormx" From 6a5b4edf1877b8433bb6765afec1d15aea8811c5 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 05:27:41 +0200 Subject: [PATCH 03/19] remove BoxFuture from trait --- ormx/src/lib.rs | 72 ++++++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/ormx/src/lib.rs b/ormx/src/lib.rs index 350a4fd..845c968 100644 --- a/ormx/src/lib.rs +++ b/ormx/src/lib.rs @@ -30,10 +30,13 @@ //! # Documentation //! See the docs of [derive(Table)](derive.Table.html) and [Patch](trait.Patch.html). -use futures::{future::BoxFuture, stream::BoxStream}; -pub use ormx_macros::*; +use std::future::Future; + +use futures::{Stream, TryStreamExt}; use sqlx::{Database, Executor, Result}; +pub use ormx_macros::*; + #[doc(hidden)] pub mod exports { pub use futures; @@ -66,7 +69,7 @@ where fn insert( db: &mut ::Connection, row: impl Insert, - ) -> BoxFuture> { + ) -> impl Future> + Send + '_ { row.insert(db) } @@ -74,69 +77,66 @@ where fn get<'a, 'c: 'a>( db: impl Executor<'c, Database = Db> + 'a, id: Self::Id, - ) -> BoxFuture<'a, Result>; + ) -> impl Future> + Send + 'a; /// Stream all rows from this table. fn stream_all<'a, 'c: 'a>( db: impl Executor<'c, Database = Db> + 'a, - ) -> BoxStream<'a, Result>; + ) -> impl Stream> + Send + 'a; fn stream_all_paginated<'a, 'c: 'a>( db: impl Executor<'c, Database = Db> + 'a, offset: i64, limit: i64, - ) -> BoxStream<'a, Result>; + ) -> impl Stream> + Send + 'a; /// Load all rows from this table. fn all<'a, 'c: 'a>( db: impl Executor<'c, Database = Db> + 'a, - ) -> BoxFuture<'a, Result>> { - use futures::TryStreamExt; - - Box::pin(Self::stream_all(db).try_collect()) + ) -> impl Future>> + Send + 'a { + Self::stream_all(db).try_collect() } fn all_paginated<'a, 'c: 'a>( db: impl Executor<'c, Database = Db> + 'a, offset: i64, limit: i64, - ) -> BoxFuture<'a, Result>> { - use futures::TryStreamExt; - - Box::pin(Self::stream_all_paginated(db, offset, limit).try_collect()) + ) -> impl Future>> + Send + 'a { + Self::stream_all_paginated(db, offset, limit).try_collect() } + /// Applies a patch to this row. fn patch<'a, 'c: 'a, P>( &'a mut self, db: impl Executor<'c, Database = Db> + 'a, patch: P, - ) -> BoxFuture<'a, Result<()>> + ) -> impl Future> + Send + 'a where P: Patch
, { - Box::pin(async move { + async move { let patch: P = patch; - patch.patch_row(db, self.id()).await?; + patch.patch_row(db, self.id()).send().await?; patch.apply_to(self); Ok(()) - }) + } } /// Updates all fields of this row, regardless if they have been changed or not. fn update<'a, 'c: 'a>( &'a self, db: impl Executor<'c, Database = Db> + 'a, - ) -> BoxFuture<'a, Result<()>>; + ) -> impl Future> + Send + 'a; - // Refresh this row, querying all columns from the database. + /// Refresh this row, querying all columns from the database. fn reload<'a, 'c: 'a>( &'a mut self, db: impl Executor<'c, Database = Db> + 'a, - ) -> BoxFuture<'a, Result<()>> { - Box::pin(async move { - *self = Self::get(db, self.id()).await?; + ) -> impl Future> + Send + 'a { + async move { + *self = Self::get(db, self.id()).send().await?; Ok(()) - }) + } } } @@ -148,13 +148,13 @@ where fn delete_row<'a, 'c: 'a>( db: impl Executor<'c, Database = Db> + 'a, id: Self::Id, - ) -> BoxFuture<'a, Result<()>>; + ) -> impl Future> + Send + 'a; /// Deletes this row from the database fn delete<'a, 'c: 'a>( self, db: impl Executor<'c, Database = Db> + 'a, - ) -> BoxFuture<'a, Result<()>> { + ) -> impl Future> + Send + 'a { Self::delete_row(db, self.id()) } @@ -162,7 +162,7 @@ where fn delete_ref<'a, 'c: 'a>( &self, db: impl Executor<'c, Database = Db> + 'a, - ) -> BoxFuture<'a, Result<()>> { + ) -> impl Future> + Send + 'a { Self::delete_row(db, self.id()) } } @@ -175,7 +175,7 @@ where type Table: Table; /// Applies the data of this patch to the given entity. - /// This does not persist the change in the database. + /// This does not persist the change in the database. fn apply_to(self, entity: &mut Self::Table); /// Applies this patch to a row in the database. @@ -183,7 +183,7 @@ where &'a self, db: impl Executor<'c, Database = Db> + 'a, id: ::Id, - ) -> BoxFuture<'a, Result<()>>; + ) -> impl Future> + Send + 'a; } /// A type which can be inserted as a row into the database. @@ -197,5 +197,17 @@ where fn insert<'a, 'c: 'a>( self, db: impl Executor<'c, Database = Db> + 'a, - ) -> BoxFuture<'a, Result>; + ) -> impl Future> + Send + 'a; } + +// Ridiculous workaround for [#100013](https://github.com/rust-lang/rust/issues/100013#issuecomment-2210995259). +trait SendFuture: Future { + fn send(self) -> impl Future + Send + where + Self: Sized + Send, + { + self + } +} + +impl SendFuture for T {} From e49d652430adeb63a91158db3ef5967eb2b7987f Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 05:47:15 +0200 Subject: [PATCH 04/19] remove all the boxes! --- ormx-macros/src/backend/common/mod.rs | 15 +++--- ormx-macros/src/backend/common/table.rs | 59 +++++++++------------- ormx-macros/src/backend/mysql/insert.rs | 19 +++---- ormx-macros/src/backend/postgres/insert.rs | 19 +++---- ormx-macros/src/utils.rs | 14 +++-- 5 files changed, 53 insertions(+), 73 deletions(-) diff --git a/ormx-macros/src/backend/common/mod.rs b/ormx-macros/src/backend/common/mod.rs index 2663562..9566d03 100644 --- a/ormx-macros/src/backend/common/mod.rs +++ b/ormx-macros/src/backend/common/mod.rs @@ -171,7 +171,6 @@ pub(crate) fn impl_patch(patch: &Patch) -> TokenStream { bindings.next().unwrap() ); - let box_future = crate::utils::box_future(); quote! { impl ormx::Patch for #patch_ident { type Table = #table_path; @@ -180,17 +179,15 @@ pub(crate) fn impl_patch(patch: &Patch) -> TokenStream { #( entity.#field_idents = self.#field_idents; )* } - fn patch_row<'a, 'c: 'a>( + async fn patch_row<'a, 'c: 'a>( &'a self, db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a, id: ::Id, - ) -> #box_future<'a, sqlx::Result<()>> { - Box::pin(async move { - sqlx::query!(#sql, #( self.#query_args, )* id) - .execute(db) - .await?; - Ok(()) - }) + ) -> sqlx::Result<()> { + sqlx::query!(#sql, #( self.#query_args, )* id) + .execute(db) + .await?; + Ok(()) } } } diff --git a/ormx-macros/src/backend/common/table.rs b/ormx-macros/src/backend/common/table.rs index 60afdf4..bc3074b 100644 --- a/ormx-macros/src/backend/common/table.rs +++ b/ormx-macros/src/backend/common/table.rs @@ -47,7 +47,6 @@ pub fn impl_delete(table: &Table) -> TokenStream { } fn get(table: &Table, column_list: &str) -> TokenStream { - let box_future = crate::utils::box_future(); let get_sql = format!( "SELECT {} FROM {} WHERE {} = {}", column_list, @@ -57,21 +56,18 @@ fn get(table: &Table, column_list: &str) -> TokenStream { ); quote! { - fn get<'a, 'c: 'a>( + async fn get<'a, 'c: 'a>( db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a, id: Self::Id, - ) -> #box_future<'a, sqlx::Result> { - Box::pin(async move { - sqlx::query_as!(Self, #get_sql, id) - .fetch_one(db) - .await - }) + ) -> sqlx::Result { + sqlx::query_as!(Self, #get_sql, id) + .fetch_one(db) + .await } } } fn update(table: &Table) -> TokenStream { - let box_future = crate::utils::box_future(); let mut bindings = B::Bindings::default(); let mut assignments = vec![]; for field in table.fields_except_id() { @@ -91,28 +87,26 @@ fn update(table: &Table) -> TokenStream { let other_arguments = table.fields_except_id().map(TableField::fmt_as_argument); quote! { - fn update<'a, 'c: 'a>( + async fn update<'a, 'c: 'a>( &'a self, db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a, - ) -> #box_future<'a, sqlx::Result<()>> { - Box::pin(async move { - sqlx::query!(#update_sql, #( #other_arguments, )* self.#id_argument) - .execute(db) - .await?; - Ok(()) - }) + ) -> sqlx::Result<()> { + sqlx::query!(#update_sql, #( #other_arguments, )* self.#id_argument) + .execute(db) + .await?; + Ok(()) } } } fn stream_all(table: &Table, column_list: &str) -> TokenStream { - let box_stream = crate::utils::box_stream(); + let return_type = crate::utils::stream!(sqlx::Result); let all_sql = format!("SELECT {} FROM {}", column_list, table.table); quote! { fn stream_all<'a, 'c: 'a>( db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a, - ) -> #box_stream<'a, sqlx::Result> { + ) -> #return_type + 'a { sqlx::query_as!(Self, #all_sql) .fetch(db) } @@ -120,7 +114,7 @@ fn stream_all(table: &Table, column_list: &str) -> TokenStream { } fn stream_all_paginated(table: &Table, column_list: &str) -> TokenStream { - let box_stream = crate::utils::box_stream(); + let return_type = crate::utils::stream!(sqlx::Result); let mut bindings = B::Bindings::default(); let all_sql = format!( "SELECT {} FROM {} LIMIT {} OFFSET {}", @@ -135,7 +129,7 @@ fn stream_all_paginated(table: &Table, column_list: &str) -> Toke db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a, offset: i64, limit: i64, - ) -> #box_stream<'a, sqlx::Result> { + ) -> #return_type + 'a { sqlx::query_as!(Self, #all_sql, limit, offset) .fetch(db) } @@ -143,7 +137,6 @@ fn stream_all_paginated(table: &Table, column_list: &str) -> Toke } fn delete(table: &Table) -> TokenStream { - let box_future = crate::utils::box_future(); let id_ty = &table.id.ty; let delete_sql = format!( "DELETE FROM {} WHERE {} = {}", @@ -159,22 +152,20 @@ fn delete(table: &Table) -> TokenStream { let result_import = quote!(sqlx::sqlite::SqliteQueryResult); quote! { - fn delete_row<'a, 'c: 'a>( + async fn delete_row<'a, 'c: 'a>( db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a, id: #id_ty - ) -> #box_future<'a, sqlx::Result<()>> { + ) -> sqlx::Result<()> { use #result_import; - Box::pin(async move { - let result = sqlx::query!(#delete_sql, id) - .execute(db) - .await?; - if result.rows_affected() == 0 { - Err(sqlx::Error::RowNotFound) - } else { - Ok(()) - } - }) + let result = sqlx::query!(#delete_sql, id) + .execute(db) + .await?; + if result.rows_affected() == 0 { + Err(sqlx::Error::RowNotFound) + } else { + Ok(()) + } } } } diff --git a/ormx-macros/src/backend/mysql/insert.rs b/ormx-macros/src/backend/mysql/insert.rs index e45cc79..030d270 100644 --- a/ormx-macros/src/backend/mysql/insert.rs +++ b/ormx-macros/src/backend/mysql/insert.rs @@ -15,7 +15,6 @@ pub fn impl_insert(table: &Table) -> TokenStream { }; let table_ident = &table.ident; - let box_future = quote!(ormx::exports::futures::future::BoxFuture); let insert = insert(&table); let query_id = query_id(&table); @@ -26,18 +25,16 @@ pub fn impl_insert(table: &Table) -> TokenStream { impl ormx::Insert for #insert_ident { type Table = #table_ident; - fn insert<'a, 'c: 'a>( + async fn insert<'a, 'c: 'a>( self, db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a, - ) -> #box_future<'a, sqlx::Result> { - Box::pin(async move { - let mut tx = db.begin().await?; - #insert - #query_id - #query_default - tx.commit().await?; - Ok(#construct_row) - }) + ) -> sqlx::Result { + let mut tx = db.begin().await?; + #insert + #query_id + #query_default + tx.commit().await?; + Ok(#construct_row) } } } diff --git a/ormx-macros/src/backend/postgres/insert.rs b/ormx-macros/src/backend/postgres/insert.rs index 875aedd..32e5a0e 100644 --- a/ormx-macros/src/backend/postgres/insert.rs +++ b/ormx-macros/src/backend/postgres/insert.rs @@ -61,24 +61,21 @@ pub fn impl_insert(table: &Table) -> TokenStream { Ident::new("fetch_one", Span::call_site()) }; - let box_future = crate::utils::box_future(); quote! { impl ormx::Insert for #insert_ident { type Table = #table_ident; - fn insert<'a, 'c: 'a>( + async fn insert<'a, 'c: 'a>( self, db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a, - ) -> #box_future<'a, sqlx::Result> { - Box::pin(async move { - let _generated = sqlx::query!(#insert_sql, #( #insert_field_exprs, )*) - .#fetch_funtion(db) - .await?; + ) -> sqlx::Result { + let _generated = sqlx::query!(#insert_sql, #( #insert_field_exprs, )*) + .#fetch_funtion(db) + .await?; - Ok(Self::Table { - #( #insert_field_idents: self.#insert_field_idents, )* - #( #default_field_idents: _generated.#default_field_idents, )* - }) + Ok(Self::Table { + #( #insert_field_idents: self.#insert_field_idents, )* + #( #default_field_idents: _generated.#default_field_idents, )* }) } } diff --git a/ormx-macros/src/utils.rs b/ormx-macros/src/utils.rs index 6106885..f8e6f69 100644 --- a/ormx-macros/src/utils.rs +++ b/ormx-macros/src/utils.rs @@ -1,14 +1,12 @@ -use proc_macro2::{Span, TokenStream}; -use quote::quote; +use proc_macro2::Span; use syn::{Error, Result}; -pub fn box_future() -> TokenStream { - quote!(ormx::exports::futures::future::BoxFuture) -} - -pub fn box_stream() -> TokenStream { - quote!(ormx::exports::futures::stream::BoxStream) +macro_rules! stream { + ($($t:tt)*) => { + quote!(impl ormx::exports::futures::stream::Stream + Send + Unpin) + }; } +pub(crate) use stream; pub fn set_once(opt: &mut Option, v: T) -> Result<()> { match opt.replace(v) { From 9d9221b21c24e92ea527655582cce11b2a4b99c9 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 06:04:58 +0200 Subject: [PATCH 05/19] cleanup --- README.md | 4 ++-- ormx-macros/src/attrs.rs | 2 +- ormx-macros/src/backend/common/table.rs | 9 ++------- ormx-macros/src/backend/mod.rs | 3 +++ ormx-macros/src/backend/mysql/insert.rs | 8 ++++---- ormx-macros/src/backend/mysql/mod.rs | 8 ++++++-- ormx-macros/src/backend/postgres/insert.rs | 4 ++-- ormx-macros/src/backend/postgres/mod.rs | 8 ++++++-- ormx-macros/src/patch/parse.rs | 4 ++-- ormx-macros/src/table/parse.rs | 2 +- 10 files changed, 29 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index e9b499d..dc40fcf 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,11 @@ Add ormx and sqlx to your `Cargo.toml` ```toml [dependencies.ormx] -version = "0.7" +version = "0.11" features = ["mysql"] [dependencies.sqlx] -version = "0.5" +version = "0.8" default-features = false features = ["macros", "mysql", "runtime-tokio-rustls"] ``` diff --git a/ormx-macros/src/attrs.rs b/ormx-macros/src/attrs.rs index bab82f2..3d90461 100644 --- a/ormx-macros/src/attrs.rs +++ b/ormx-macros/src/attrs.rs @@ -172,7 +172,7 @@ impl_parse!(PatchFieldAttr { }); pub struct AnyAttribute(pub Vec); -impl syn::parse::Parse for AnyAttribute { +impl Parse for AnyAttribute { fn parse(input: ParseStream) -> Result { input.call(Attribute::parse_outer).map(Self) } diff --git a/ormx-macros/src/backend/common/table.rs b/ormx-macros/src/backend/common/table.rs index bc3074b..0ecd6cb 100644 --- a/ormx-macros/src/backend/common/table.rs +++ b/ormx-macros/src/backend/common/table.rs @@ -144,19 +144,14 @@ fn delete(table: &Table) -> TokenStream { table.id.column(), B::Bindings::default().next().unwrap() ); - #[cfg(feature = "mysql")] - let result_import = quote!(sqlx::mysql::MySqlQueryResult); - #[cfg(feature = "postgres")] - let result_import = quote!(sqlx::postgres::PgQueryResult); - #[cfg(feature = "sqlite")] - let result_import = quote!(sqlx::sqlite::SqliteQueryResult); + let query_result = B::query_result(); quote! { async fn delete_row<'a, 'c: 'a>( db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a, id: #id_ty ) -> sqlx::Result<()> { - use #result_import; + use #query_result; let result = sqlx::query!(#delete_sql, id) .execute(db) diff --git a/ormx-macros/src/backend/mod.rs b/ormx-macros/src/backend/mod.rs index 37b4592..7dfeeb0 100644 --- a/ormx-macros/src/backend/mod.rs +++ b/ormx-macros/src/backend/mod.rs @@ -24,6 +24,9 @@ pub trait Backend: Sized + Clone { type Bindings: Iterator> + Default; + /// Returns the type used to return query results, e.g `sqlx::postgres::PgQueryResult` + fn query_result() -> TokenStream; + /// Generate an `impl
` block, containing getter methods fn impl_getters(table: &Table) -> TokenStream { common::getters::(table) diff --git a/ormx-macros/src/backend/mysql/insert.rs b/ormx-macros/src/backend/mysql/insert.rs index 030d270..1b10c4b 100644 --- a/ormx-macros/src/backend/mysql/insert.rs +++ b/ormx-macros/src/backend/mysql/insert.rs @@ -16,10 +16,10 @@ pub fn impl_insert(table: &Table) -> TokenStream { let table_ident = &table.ident; - let insert = insert(&table); - let query_id = query_id(&table); - let query_default = query_default(&table); - let construct_row = construct_row(&table); + let insert = insert(table); + let query_id = query_id(table); + let query_default = query_default(table); + let construct_row = construct_row(table); quote! { impl ormx::Insert for #insert_ident { diff --git a/ormx-macros/src/backend/mysql/mod.rs b/ormx-macros/src/backend/mysql/mod.rs index 8ec8dde..5d24e54 100644 --- a/ormx-macros/src/backend/mysql/mod.rs +++ b/ormx-macros/src/backend/mysql/mod.rs @@ -1,8 +1,8 @@ use std::borrow::Cow; -use proc_macro2::TokenStream; - use crate::{backend::Backend, table::Table}; +use proc_macro2::TokenStream; +use quote::quote; mod insert; @@ -14,6 +14,10 @@ impl Backend for MySqlBackend { const RESERVED_IDENTS: &'static [&'static str] = &[]; type Bindings = MySqlBindings; + fn query_result() -> TokenStream { + quote!(sqlx::mysql::MySqlQueryResult) + } + fn impl_insert(table: &Table) -> TokenStream { insert::impl_insert(table) } diff --git a/ormx-macros/src/backend/postgres/insert.rs b/ormx-macros/src/backend/postgres/insert.rs index 32e5a0e..b9de92a 100644 --- a/ormx-macros/src/backend/postgres/insert.rs +++ b/ormx-macros/src/backend/postgres/insert.rs @@ -55,7 +55,7 @@ pub fn impl_insert(table: &Table) -> TokenStream { .map(|f| f.fmt_as_argument()) .collect::>(); - let fetch_funtion = if default_fields.is_empty() { + let fetch_fn = if default_fields.is_empty() { Ident::new("execute", Span::call_site()) } else { Ident::new("fetch_one", Span::call_site()) @@ -70,7 +70,7 @@ pub fn impl_insert(table: &Table) -> TokenStream { db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a, ) -> sqlx::Result { let _generated = sqlx::query!(#insert_sql, #( #insert_field_exprs, )*) - .#fetch_funtion(db) + .#fetch_fn(db) .await?; Ok(Self::Table { diff --git a/ormx-macros/src/backend/postgres/mod.rs b/ormx-macros/src/backend/postgres/mod.rs index 2f6d556..740066f 100644 --- a/ormx-macros/src/backend/postgres/mod.rs +++ b/ormx-macros/src/backend/postgres/mod.rs @@ -1,8 +1,8 @@ use std::borrow::Cow; -use proc_macro2::TokenStream; - use crate::{backend::Backend, table::Table}; +use proc_macro2::TokenStream; +use quote::quote; mod insert; @@ -28,6 +28,10 @@ impl Backend for PgBackend { ]; type Bindings = PgBindings; + fn query_result() -> TokenStream { + quote!(sqlx::postgres::PgQueryResult) + } + fn impl_insert(table: &Table) -> TokenStream { insert::impl_insert(table) } diff --git a/ormx-macros/src/patch/parse.rs b/ormx-macros/src/patch/parse.rs index 6a9c85e..5fce176 100644 --- a/ormx-macros/src/patch/parse.rs +++ b/ormx-macros/src/patch/parse.rs @@ -9,7 +9,7 @@ use crate::{ utils::{missing_attr, set_once}, }; -impl TryFrom<&syn::DeriveInput> for Patch { +impl TryFrom<&DeriveInput> for Patch { type Error = Error; fn try_from(value: &DeriveInput) -> Result { @@ -45,7 +45,7 @@ impl TryFrom<&syn::DeriveInput> for Patch { } } -impl TryFrom<&syn::Field> for PatchField { +impl TryFrom<&Field> for PatchField { type Error = Error; fn try_from(value: &Field) -> Result { diff --git a/ormx-macros/src/table/parse.rs b/ormx-macros/src/table/parse.rs index 008972c..5f6734a 100644 --- a/ormx-macros/src/table/parse.rs +++ b/ormx-macros/src/table/parse.rs @@ -74,7 +74,7 @@ impl TryFrom<&syn::Field> for TableField { } } -impl TryFrom<&syn::DeriveInput> for Table { +impl TryFrom<&DeriveInput> for Table { type Error = Error; fn try_from(value: &DeriveInput) -> Result { From 8365098b21fae15c801b94afdff51796e180daf2 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 07:29:37 +0200 Subject: [PATCH 06/19] get MariaDB working again --- example-mysql/src/main.rs | 9 +- example-postgres/src/main.rs | 5 +- ormx-macros/src/backend/mod.rs | 2 +- ormx-macros/src/backend/mysql/insert.rs | 147 +++++++-------------- ormx-macros/src/backend/mysql/mod.rs | 2 + ormx-macros/src/backend/postgres/insert.rs | 59 ++++----- ormx-macros/src/utils.rs | 2 +- ormx/src/lib.rs | 2 +- ormx/src/query2/map.rs | 4 +- 9 files changed, 82 insertions(+), 150 deletions(-) diff --git a/example-mysql/src/main.rs b/example-mysql/src/main.rs index da7a13e..f344fb9 100644 --- a/example-mysql/src/main.rs +++ b/example-mysql/src/main.rs @@ -1,12 +1,9 @@ -// #![feature(trace_macros)] use chrono::{NaiveDateTime, Utc}; use log::LevelFilter; -use ormx::{Insert, Table}; +use ormx::{Delete, Insert, Table}; use sqlx::MySqlPool; -// trace_macros!(true); - -// To run this example-postgres, first run `/scripts/postgres.sh` to start postgres in a docker container and +// To run this example, first run `/scripts/mariadb.sh` to start mariadb in a docker container and // write the database URL to `.env`. Then, source `.env` (`. .env`) and run `cargo run` mod query2; @@ -63,7 +60,7 @@ async fn main() -> anyhow::Result<()> { } #[derive(Debug, ormx::Table)] -#[ormx(table = "users", id = user_id, insertable)] +#[ormx(table = "users", id = user_id, insertable, deletable)] struct User { // map this field to the column "id" #[ormx(column = "id")] diff --git a/example-postgres/src/main.rs b/example-postgres/src/main.rs index 25b51a0..5d1125b 100644 --- a/example-postgres/src/main.rs +++ b/example-postgres/src/main.rs @@ -1,12 +1,9 @@ -// #![feature(trace_macros)] use chrono::{NaiveDateTime, Utc}; use log::LevelFilter; use ormx::{Delete, Insert, Table}; use sqlx::PgPool; -// trace_macros!(true); - -// To run this example-postgres, first run `/scripts/postgres.sh` to start postgres in a docker container and +// To run this example, first run `/scripts/postgres.sh` to start postgres in a docker container and // write the database URL to `.env`. Then, source `.env` (`. .env`) and run `cargo run` mod query2; diff --git a/ormx-macros/src/backend/mod.rs b/ormx-macros/src/backend/mod.rs index 7dfeeb0..abc049b 100644 --- a/ormx-macros/src/backend/mod.rs +++ b/ormx-macros/src/backend/mod.rs @@ -19,8 +19,8 @@ compile_error!("sqlite is currently not supported"); pub trait Backend: Sized + Clone { const QUOTE: char; - /// TODO: benchmark HashSet vs linear search const RESERVED_IDENTS: &'static [&'static str]; + const IS_MYSQL: bool = false; type Bindings: Iterator> + Default; diff --git a/ormx-macros/src/backend/mysql/insert.rs b/ormx-macros/src/backend/mysql/insert.rs index 1b10c4b..24da002 100644 --- a/ormx-macros/src/backend/mysql/insert.rs +++ b/ormx-macros/src/backend/mysql/insert.rs @@ -1,12 +1,10 @@ use itertools::Itertools; -use proc_macro2::TokenStream; +use proc_macro2::{Span, TokenStream}; use quote::quote; +use syn::Ident; -use super::MySqlBackend; -use crate::{ - backend::mysql::MySqlBindings, - table::{Table, TableField}, -}; +use crate::backend::mysql::{MySqlBackend, MySqlBindings}; +use crate::table::{Table, TableField}; pub fn impl_insert(table: &Table) -> TokenStream { let insert_ident = match &table.insertable { @@ -14,12 +12,23 @@ pub fn impl_insert(table: &Table) -> TokenStream { None => return quote!(), }; + let insert_fields: Vec<&TableField> = table.insertable_fields().collect(); + let default_fields: Vec<&TableField> = table.default_fields().collect(); + let table_ident = &table.ident; + let insert_field_idents = insert_fields.iter().map(|field| &field.field); + let default_field_idents = default_fields.iter().map(|field| &field.field); + let default_field_ordinals = (0usize..).take(default_fields.len()); - let insert = insert(table); - let query_id = query_id(table); - let query_default = query_default(table); - let construct_row = construct_row(table); + let insert_sql = insert_sql(table, &insert_fields); + + let insert_field_exprs = insert_fields.iter().map(|f| f.fmt_as_argument()); + + let fetch_fn = if default_fields.is_empty() { + Ident::new("execute", Span::call_site()) + } else { + Ident::new("fetch_one", Span::call_site()) + }; quote! { impl ormx::Insert for #insert_ident { @@ -29,102 +38,40 @@ pub fn impl_insert(table: &Table) -> TokenStream { self, db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a, ) -> sqlx::Result { - let mut tx = db.begin().await?; - #insert - #query_id - #query_default - tx.commit().await?; - Ok(#construct_row) - } - } - } -} + use sqlx::Row; -/// build an instance of the table struct from -/// - `_id` (see `query_id` below) -/// - `_generated` (see `query_default` below) -/// - all fields already present in the insert struct -fn construct_row(table: &Table) -> TokenStream { - let id_ident = &table.id.field; - let insert_field_idents = table - .insertable_fields() - .map(|f| &f.field) - .filter(|f| *f != id_ident); - let default_field_idents = table - .default_fields() - .map(|f| &f.field) - .filter(|f| *f != id_ident); + let _generated = sqlx::query!(#insert_sql, #( #insert_field_exprs, )*) + .#fetch_fn(db) + .await?; - quote! { - Self::Table { - #id_ident: _id as _, - #( #insert_field_idents: self.#insert_field_idents, )* - #( #default_field_idents: _generated.#default_field_idents, )* + Ok(Self::Table { + #( #insert_field_idents: self.#insert_field_idents, )* + #( #default_field_idents: _generated.get(#default_field_ordinals), )* + }) + } } } } -/// queries default fields from the database, except the ID. -fn query_default(table: &Table) -> TokenStream { - let mut default_fields = table +fn insert_sql(table: &Table, insert_fields: &[&TableField]) -> String { + let columns = insert_fields.iter().map(|field| field.column()).join(", "); + let fields = MySqlBindings::default() + .take(insert_fields.len()) + .join(", "); + let returning_fields = table .default_fields() - .filter(|f| f.field != table.id.field) - .peekable(); - - if default_fields.peek().is_none() { - return quote!(); - } - - let query_default_sql = format!( - "SELECT {} FROM {} WHERE {} = ?", - default_fields.map(TableField::fmt_for_select).join(", "), - table.table, - table.id.column() - ); - - quote! { - let _generated = sqlx::query!(#query_default_sql, _id) - .fetch_one(&mut tx) - .await?; - } -} - -/// inserts the struct into the database -fn insert(table: &Table) -> TokenStream { - let insert_fields: Vec<_> = table.insertable_fields().collect(); - let insert_field_idents = insert_fields.iter().map(|field| &field.field); - - let insert_sql = format!( - "INSERT INTO {} ({}) VALUES ({})", - table.table, - insert_fields.iter().map(|field| field.column()).join(", "), - MySqlBindings.take(insert_fields.len()).join(", ") - ); - - quote! { - sqlx::query!(#insert_sql, #( self.#insert_field_idents, )*) - .execute(&mut tx) - .await?; - } -} - -/// obtains the id of the inserted row. -/// -/// case 1: -/// The ID is database generated, so we query it with LAST_INSERT_ID -/// case 2: -/// The ID is already known, so we can just use it. -fn query_id(table: &Table) -> TokenStream { - match table.id.default { - true => quote! { - let _id = sqlx::query!("SELECT LAST_INSERT_ID() AS id") - .fetch_one(&mut tx) - .await? - .id; - }, - false => { - let id_ident = &table.id.field; - quote!(let _id = self.#id_ident;) - } + .map(TableField::fmt_for_select) + .join(", "); + + if returning_fields.is_empty() { + format!( + "INSERT INTO {} ({}) VALUES ({})", + table.table, columns, fields + ) + } else { + format!( + "INSERT INTO {} ({}) VALUES ({}) RETURNING {}", + table.table, columns, fields, returning_fields + ) } } diff --git a/ormx-macros/src/backend/mysql/mod.rs b/ormx-macros/src/backend/mysql/mod.rs index 5d24e54..2e8ae71 100644 --- a/ormx-macros/src/backend/mysql/mod.rs +++ b/ormx-macros/src/backend/mysql/mod.rs @@ -12,6 +12,8 @@ pub struct MySqlBackend; impl Backend for MySqlBackend { const QUOTE: char = '`'; const RESERVED_IDENTS: &'static [&'static str] = &[]; + const IS_MYSQL: bool = true; + type Bindings = MySqlBindings; fn query_result() -> TokenStream { diff --git a/ormx-macros/src/backend/postgres/insert.rs b/ormx-macros/src/backend/postgres/insert.rs index b9de92a..8194288 100644 --- a/ormx-macros/src/backend/postgres/insert.rs +++ b/ormx-macros/src/backend/postgres/insert.rs @@ -8,27 +8,6 @@ use crate::{ table::{Table, TableField}, }; -fn insert_sql(table: &Table, insert_fields: &[&TableField]) -> String { - let columns = insert_fields.iter().map(|field| field.column()).join(", "); - let fields = PgBindings::default().take(insert_fields.len()).join(", "); - let returning_fields = table - .default_fields() - .map(TableField::fmt_for_select) - .join(", "); - - if returning_fields.is_empty() { - format!( - "INSERT INTO {} ({}) VALUES ({})", - table.table, columns, fields - ) - } else { - format!( - "INSERT INTO {} ({}) VALUES ({}) RETURNING {}", - table.table, columns, fields, returning_fields - ) - } -} - pub fn impl_insert(table: &Table) -> TokenStream { let insert_ident = match &table.insertable { Some(i) => &i.ident, @@ -39,21 +18,10 @@ pub fn impl_insert(table: &Table) -> TokenStream { let default_fields: Vec<&TableField> = table.default_fields().collect(); let table_ident = &table.ident; - let insert_field_idents = insert_fields - .iter() - .map(|field| &field.field) - .collect::>(); - let default_field_idents = default_fields - .iter() - .map(|field| &field.field) - .collect::>(); - + let insert_field_idents = insert_fields.iter().map(|field| &field.field); + let default_field_idents = default_fields.iter().map(|field| &field.field); let insert_sql = insert_sql(table, &insert_fields); - - let insert_field_exprs = insert_fields - .iter() - .map(|f| f.fmt_as_argument()) - .collect::>(); + let insert_field_exprs = insert_fields.iter().map(|f| f.fmt_as_argument()); let fetch_fn = if default_fields.is_empty() { Ident::new("execute", Span::call_site()) @@ -81,3 +49,24 @@ pub fn impl_insert(table: &Table) -> TokenStream { } } } + +fn insert_sql(table: &Table, insert_fields: &[&TableField]) -> String { + let columns = insert_fields.iter().map(|field| field.column()).join(", "); + let fields = PgBindings::default().take(insert_fields.len()).join(", "); + let returning_fields = table + .default_fields() + .map(TableField::fmt_for_select) + .join(", "); + + if returning_fields.is_empty() { + format!( + "INSERT INTO {} ({}) VALUES ({})", + table.table, columns, fields + ) + } else { + format!( + "INSERT INTO {} ({}) VALUES ({}) RETURNING {}", + table.table, columns, fields, returning_fields + ) + } +} diff --git a/ormx-macros/src/utils.rs b/ormx-macros/src/utils.rs index f8e6f69..fafbf9a 100644 --- a/ormx-macros/src/utils.rs +++ b/ormx-macros/src/utils.rs @@ -3,7 +3,7 @@ use syn::{Error, Result}; macro_rules! stream { ($($t:tt)*) => { - quote!(impl ormx::exports::futures::stream::Stream + Send + Unpin) + quote!(impl ormx::exports::Stream + Send + Unpin) }; } pub(crate) use stream; diff --git a/ormx/src/lib.rs b/ormx/src/lib.rs index 845c968..1a4516e 100644 --- a/ormx/src/lib.rs +++ b/ormx/src/lib.rs @@ -39,7 +39,7 @@ pub use ormx_macros::*; #[doc(hidden)] pub mod exports { - pub use futures; + pub use futures::Stream; pub use crate::query2::map::*; } diff --git a/ormx/src/query2/map.rs b/ormx/src/query2/map.rs index 99ab0cd..203ab51 100644 --- a/ormx/src/query2/map.rs +++ b/ormx/src/query2/map.rs @@ -1,4 +1,4 @@ -use futures::stream::BoxStream; +use futures::Stream; use sqlx::{Database, Executor, IntoArguments, Error, query::Map}; #[doc(hidden)] @@ -45,7 +45,7 @@ macro_rules! make_conditional_map_ty { $fa: 'q + Send + IntoArguments<'q, DB>, )* { - pub fn fetch<'e, 'c: 'e, E>(self, executor: E) -> BoxStream<'e, sqlx::Result> + pub fn fetch<'e, 'c: 'e, E>(self, executor: E) -> impl Stream> + Send + Unpin + 'e where 'q: 'e, E: 'e + Executor<'c, Database = DB>, From 8c662704027fbce000942cac3468142a30925a22 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 07:45:54 +0200 Subject: [PATCH 07/19] insert: use impl Executor --- ormx/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ormx/src/lib.rs b/ormx/src/lib.rs index 1a4516e..4938ab5 100644 --- a/ormx/src/lib.rs +++ b/ormx/src/lib.rs @@ -33,7 +33,7 @@ use std::future::Future; use futures::{Stream, TryStreamExt}; -use sqlx::{Database, Executor, Result}; +use sqlx::{Executor, Result}; pub use ormx_macros::*; @@ -66,10 +66,10 @@ where fn id(&self) -> Self::Id; /// Insert a row into the database. - fn insert( - db: &mut ::Connection, + fn insert<'a, 'c: 'a>( + db: impl Executor<'c, Database = Db> + 'a, row: impl Insert
, - ) -> impl Future> + Send + '_ { + ) -> impl Future> + Send + 'a { row.insert(db) } From 94ec5cbc4c5910a4359d0e1368703ff67a3602fd Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 08:07:19 +0200 Subject: [PATCH 08/19] always escape all table/field names --- ormx-macros/src/backend/common/mod.rs | 4 +-- ormx-macros/src/backend/common/table.rs | 10 +++---- ormx-macros/src/backend/mod.rs | 1 - ormx-macros/src/backend/mysql/insert.rs | 15 +++++++--- ormx-macros/src/backend/mysql/mod.rs | 4 +-- ormx-macros/src/backend/postgres/insert.rs | 9 ++++-- ormx-macros/src/backend/postgres/mod.rs | 18 ++--------- ormx-macros/src/table/mod.rs | 35 ++++++++++------------ ormx-macros/src/table/parse.rs | 9 ------ ormx/src/lib.rs | 3 +- 10 files changed, 46 insertions(+), 62 deletions(-) diff --git a/ormx-macros/src/backend/common/mod.rs b/ormx-macros/src/backend/common/mod.rs index 9566d03..f6627bc 100644 --- a/ormx-macros/src/backend/common/mod.rs +++ b/ormx-macros/src/backend/common/mod.rs @@ -23,7 +23,7 @@ pub(crate) fn getters(table: &Table) -> TokenStream { let sql = format!( "SELECT {} FROM {} WHERE {} = {}", column_list, - table.table, + table.name(), field.column(), B::Bindings::default().next().unwrap() ); @@ -103,7 +103,7 @@ pub fn setters(table: &Table) -> TokenStream { let mut bindings = B::Bindings::default(); let sql = format!( "UPDATE {} SET {} = {} WHERE {} = {}", - table.table, + table.name(), field.column(), bindings.next().unwrap(), table.id.column(), diff --git a/ormx-macros/src/backend/common/table.rs b/ormx-macros/src/backend/common/table.rs index 0ecd6cb..a674aad 100644 --- a/ormx-macros/src/backend/common/table.rs +++ b/ormx-macros/src/backend/common/table.rs @@ -50,7 +50,7 @@ fn get(table: &Table, column_list: &str) -> TokenStream { let get_sql = format!( "SELECT {} FROM {} WHERE {} = {}", column_list, - table.table, + table.name(), table.id.column(), B::Bindings::default().next().unwrap() ); @@ -78,7 +78,7 @@ fn update(table: &Table) -> TokenStream { let update_sql = format!( "UPDATE {} SET {} WHERE {} = {}", - table.table, + table.name(), assignments, table.id.column(), bindings.next().unwrap() @@ -101,7 +101,7 @@ fn update(table: &Table) -> TokenStream { fn stream_all(table: &Table, column_list: &str) -> TokenStream { let return_type = crate::utils::stream!(sqlx::Result); - let all_sql = format!("SELECT {} FROM {}", column_list, table.table); + let all_sql = format!("SELECT {} FROM {}", column_list, table.name()); quote! { fn stream_all<'a, 'c: 'a>( @@ -119,7 +119,7 @@ fn stream_all_paginated(table: &Table, column_list: &str) -> Toke let all_sql = format!( "SELECT {} FROM {} LIMIT {} OFFSET {}", column_list, - table.table, + table.name(), bindings.next().unwrap(), bindings.next().unwrap() ); @@ -140,7 +140,7 @@ fn delete(table: &Table) -> TokenStream { let id_ty = &table.id.ty; let delete_sql = format!( "DELETE FROM {} WHERE {} = {}", - table.table, + table.name(), table.id.column(), B::Bindings::default().next().unwrap() ); diff --git a/ormx-macros/src/backend/mod.rs b/ormx-macros/src/backend/mod.rs index abc049b..9bde9df 100644 --- a/ormx-macros/src/backend/mod.rs +++ b/ormx-macros/src/backend/mod.rs @@ -19,7 +19,6 @@ compile_error!("sqlite is currently not supported"); pub trait Backend: Sized + Clone { const QUOTE: char; - const RESERVED_IDENTS: &'static [&'static str]; const IS_MYSQL: bool = false; type Bindings: Iterator> + Default; diff --git a/ormx-macros/src/backend/mysql/insert.rs b/ormx-macros/src/backend/mysql/insert.rs index 24da002..f2c194c 100644 --- a/ormx-macros/src/backend/mysql/insert.rs +++ b/ormx-macros/src/backend/mysql/insert.rs @@ -3,8 +3,10 @@ use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::Ident; -use crate::backend::mysql::{MySqlBackend, MySqlBindings}; -use crate::table::{Table, TableField}; +use crate::{ + backend::mysql::{MySqlBackend, MySqlBindings}, + table::{Table, TableField}, +}; pub fn impl_insert(table: &Table) -> TokenStream { let insert_ident = match &table.insertable { @@ -66,12 +68,17 @@ fn insert_sql(table: &Table, insert_fields: &[&TableField, insert_fields: &[&TableField] if returning_fields.is_empty() { format!( "INSERT INTO {} ({}) VALUES ({})", - table.table, columns, fields + table.name(), + columns, + fields ) } else { format!( "INSERT INTO {} ({}) VALUES ({}) RETURNING {}", - table.table, columns, fields, returning_fields + table.name(), + columns, + fields, + returning_fields ) } } diff --git a/ormx-macros/src/backend/postgres/mod.rs b/ormx-macros/src/backend/postgres/mod.rs index 740066f..af2c6f8 100644 --- a/ormx-macros/src/backend/postgres/mod.rs +++ b/ormx-macros/src/backend/postgres/mod.rs @@ -1,9 +1,10 @@ use std::borrow::Cow; -use crate::{backend::Backend, table::Table}; use proc_macro2::TokenStream; use quote::quote; +use crate::{backend::Backend, table::Table}; + mod insert; #[derive(Clone)] @@ -11,21 +12,6 @@ pub struct PgBackend; impl Backend for PgBackend { const QUOTE: char = '"'; - #[rustfmt::skip] - const RESERVED_IDENTS: &'static [&'static str] = &[ - "ALL", "ANALYSE", "ANALYZE", "AND", "ANY", "ARRAY", "AS", "ASC", "ASYMMETRIC", - "AUTHORIZATION", "BETWEEN", "BINARY", "BOTH", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", - "CONSTRAINT", "CREATE", "CROSS", "CURRENT_DATE", "CURRENT_ROLE", "CURRENT_TIME", - "CURRENT_TIMESTAMP", "CURRENT_USER", "DEFAULT", "DEFERRABLE", "DESC", "DISTINCT", "DO", - "ELSE", "END", "EXCEPT", "FALSE", "FETCH", "FOR", "FOREIGN", "FROM", "FREEZE", "FULL", - "GRANT", "GROUP", "HAVING", "ILIKE", "IN", "INITIALLY", "INNER", "INTERSECT", "INTO", "IS", - "ISNULL", "JOIN", "LATERAL", "LEADING", "LEFT", "LIMIT", "LIKE", "LOCALTIME", - "LOCALTIMESTAMP", "NATURAL", "NOT", "NOTNULL", "NULL", "OFFSET", "ON", "ONLY", "OR", - "ORDER", "OUTER", "OVERLAPS", "PLACING", "PRIMARY", "REFERENCES", "RETURNING", "RIGHT", - "SELECT", "SESSION_USER", "SIMILAR", "SOME", "SYMMETRIC", "TABLE", "TABLESAMPLE", "THEN", - "TO", "TRAILING", "TRUE", "UNION", "UNIQUE", "USER", "USING", "VARIADIC", "VERBOSE", "WHEN", - "WHERE", "WINDOW", "WITH" - ]; type Bindings = PgBindings; fn query_result() -> TokenStream { diff --git a/ormx-macros/src/table/mod.rs b/ormx-macros/src/table/mod.rs index 05be44d..91c0e4e 100644 --- a/ormx-macros/src/table/mod.rs +++ b/ormx-macros/src/table/mod.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, convert::TryFrom, marker::PhantomData}; +use std::{convert::TryFrom, marker::PhantomData}; use itertools::Itertools; use proc_macro2::{Ident, Span, TokenStream}; @@ -15,7 +15,7 @@ mod parse; pub struct Table { pub ident: Ident, pub vis: Visibility, - pub table: String, + table: String, pub id: TableField, pub fields: Vec>, pub insertable: Option, @@ -26,9 +26,8 @@ pub struct Table { pub struct TableField { pub field: Ident, pub ty: Type, - pub column_name: String, + column_name: String, pub custom_type: bool, - pub reserved_ident: bool, pub default: bool, pub get_one: Option, pub get_optional: Option, @@ -59,22 +58,23 @@ impl Table { .map(|field| field.fmt_for_select()) .join(", ") } + + pub fn name(&self) -> String { + let q = B::QUOTE; + format!("{q}{}{q}", self.table) + } } impl TableField { pub fn fmt_for_select(&self) -> String { + let q = B::QUOTE; + if self.custom_type { - format!( - "{} AS {}{}: _{}", - self.column(), - B::QUOTE, - self.field, - B::QUOTE - ) + format!("{q}{}{q} AS {q}{}: _{q}", self.column_name, self.field,) } else if self.field == self.column_name { - self.column().into() + self.column() } else { - format!("{} AS {}", self.column(), self.field) + format!("{q}{}{q} AS {q}{}{q}", self.column_name, self.field) } } @@ -95,12 +95,9 @@ impl TableField { out } - pub fn column(&self) -> Cow { - if self.reserved_ident { - format!("{}{}{}", B::QUOTE, self.column_name, B::QUOTE).into() - } else { - Cow::Borrowed(&self.column_name) - } + pub fn column(&self) -> String { + let q = B::QUOTE; + format!("{q}{}{q}", self.column_name) } } diff --git a/ormx-macros/src/table/parse.rs b/ormx-macros/src/table/parse.rs index 5f6734a..48a8b63 100644 --- a/ormx-macros/src/table/parse.rs +++ b/ormx-macros/src/table/parse.rs @@ -20,14 +20,6 @@ impl TryFrom<&syn::Field> for TableField { fn try_from(value: &syn::Field) -> Result { let ident = value.ident.clone().unwrap(); - let reserved_ident = B::RESERVED_IDENTS.contains(&&*ident.to_string().to_uppercase()); - if reserved_ident { - proc_macro_error::emit_warning!( - ident.span(), - "This is a reserved keyword, you might want to consider choosing a different name." - ); - } - none!( column, custom_type, @@ -61,7 +53,6 @@ impl TryFrom<&syn::Field> for TableField { field: ident, ty: value.ty.clone(), custom_type: custom_type.unwrap_or(false), - reserved_ident, default: default.unwrap_or(false), get_one, get_optional, diff --git a/ormx/src/lib.rs b/ormx/src/lib.rs index 4938ab5..ff4e399 100644 --- a/ormx/src/lib.rs +++ b/ormx/src/lib.rs @@ -33,9 +33,8 @@ use std::future::Future; use futures::{Stream, TryStreamExt}; -use sqlx::{Executor, Result}; - pub use ormx_macros::*; +use sqlx::{Executor, Result}; #[doc(hidden)] pub mod exports { From db346ab81d2b169a170d0007122942929c0ba221 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 08:22:20 +0200 Subject: [PATCH 09/19] Add CI for examples --- .github/workflows/examples.yml | 78 +++++++++++++++++++ .../migrations/20240908062140_schema.sql | 9 +++ .../migrations/20240908062042_schema.sql | 18 +++++ 3 files changed, 105 insertions(+) create mode 100644 .github/workflows/examples.yml create mode 100644 example-mysql/migrations/20240908062140_schema.sql create mode 100644 example-postgres/migrations/20240908062042_schema.sql diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml new file mode 100644 index 0000000..56154e1 --- /dev/null +++ b/.github/workflows/examples.yml @@ -0,0 +1,78 @@ +name: Examples + +on: + pull_request: + push: + branches: + - main + - '*-dev' + +jobs: + mysql: + name: MySQL Example + runs-on: ubuntu-latest + + services: + mysql: + image: mysql:latest + env: + MYSQL_ROOT_PASSWORD: password + ports: + - 3306:3306 + + steps: + - uses: actions/checkout@v4 + + - uses: Swatinem/rust-cache@v2 + with: + key: mysql-examples + + - name: Install sqlx-cli + run: cargo install sqlx-cli + + - name: Setup + working-directory: example-mysql + env: + DATABASE_URL: mysql://root:password@localhost:3306/ormx?ssl-mode=disabled + run: sqlx db setup + + - name: Run + working-directory: example-mysql + env: + DATABASE_URL: mysql://root:password@localhost:3306/ormx?ssl-mode=disabled + run: cargo run + + postgres: + name: PostgreSQL Example + runs-on: ubuntu-latest + needs: sqlx-cli + + services: + postgres: + image: postgres:latest + env: + POSTGRES_PASSWORD: password + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v4 + + - uses: Swatinem/rust-cache@v2 + with: + key: postgres-example + + - name: Install sqlx-cli + run: cargo install sqlx-cli + + - name: Setup + working-directory: example-postgres + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/ormx + run: sqlx db setup + + - name: Run + working-directory: example-postgres + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/ormx + run: cargo run diff --git a/example-mysql/migrations/20240908062140_schema.sql b/example-mysql/migrations/20240908062140_schema.sql new file mode 100644 index 0000000..773172b --- /dev/null +++ b/example-mysql/migrations/20240908062140_schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE users +( + id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + first_name VARCHAR(128) NOT NULL, + last_name VARCHAR(128) NOT NULL, + email VARCHAR(128) NOT NULL UNIQUE, + disabled VARCHAR(128), + last_login DATETIME DEFAULT NULL +) \ No newline at end of file diff --git a/example-postgres/migrations/20240908062042_schema.sql b/example-postgres/migrations/20240908062042_schema.sql new file mode 100644 index 0000000..b9e0069 --- /dev/null +++ b/example-postgres/migrations/20240908062042_schema.sql @@ -0,0 +1,18 @@ +CREATE TYPE user_role AS ENUM ('user', 'admin'); + +CREATE TABLE users +( + id SERIAL PRIMARY KEY, + first_name VARCHAR(128) NOT NULL, + last_name VARCHAR(128) NOT NULL, + email VARCHAR(128) NOT NULL UNIQUE, + role user_role NOT NULL, + disabled TEXT, + last_login TIMESTAMP DEFAULT NULL +); + +CREATE TABLE test +( + id SERIAL PRIMARY KEY, + rows TEXT[] NOT NULL +); \ No newline at end of file From a0df67a9409b30217ede395723cf5d972625814f Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 08:24:01 +0200 Subject: [PATCH 10/19] fix CI --- .github/workflows/examples.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 56154e1..221c38b 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -45,7 +45,6 @@ jobs: postgres: name: PostgreSQL Example runs-on: ubuntu-latest - needs: sqlx-cli services: postgres: From a088991b4ae32034a8e1ff78bce60e64c1a02686 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 08:40:46 +0200 Subject: [PATCH 11/19] fix CI --- .github/workflows/examples.yml | 12 +++++------ Cargo.toml | 2 +- {example-mysql => example-mariadb}/Cargo.toml | 0 .../migrations/20240908062140_schema.sql | 0 .../src/main.rs | 0 .../src/query2.rs | 0 scripts/mariadb.sh | 17 +++++++--------- scripts/postgres.sh | 20 +++++++++---------- 8 files changed, 23 insertions(+), 28 deletions(-) rename {example-mysql => example-mariadb}/Cargo.toml (100%) rename {example-mysql => example-mariadb}/migrations/20240908062140_schema.sql (100%) rename {example-mysql => example-mariadb}/src/main.rs (100%) rename {example-mysql => example-mariadb}/src/query2.rs (100%) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 221c38b..99c4cc1 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -8,13 +8,13 @@ on: - '*-dev' jobs: - mysql: - name: MySQL Example + mariadb: + name: MariaDB Example runs-on: ubuntu-latest services: mysql: - image: mysql:latest + image: mariadb:latest env: MYSQL_ROOT_PASSWORD: password ports: @@ -25,19 +25,19 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - key: mysql-examples + key: mariadb-examples - name: Install sqlx-cli run: cargo install sqlx-cli - name: Setup - working-directory: example-mysql + working-directory: example-mariadb env: DATABASE_URL: mysql://root:password@localhost:3306/ormx?ssl-mode=disabled run: sqlx db setup - name: Run - working-directory: example-mysql + working-directory: example-mariadb env: DATABASE_URL: mysql://root:password@localhost:3306/ormx?ssl-mode=disabled run: cargo run diff --git a/Cargo.toml b/Cargo.toml index e109007..17ad93d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["ormx-macros", "ormx", "example-postgres", "example-mysql"] +members = ["ormx-macros", "ormx", "example-postgres", "example-mariadb"] resolver = "2" \ No newline at end of file diff --git a/example-mysql/Cargo.toml b/example-mariadb/Cargo.toml similarity index 100% rename from example-mysql/Cargo.toml rename to example-mariadb/Cargo.toml diff --git a/example-mysql/migrations/20240908062140_schema.sql b/example-mariadb/migrations/20240908062140_schema.sql similarity index 100% rename from example-mysql/migrations/20240908062140_schema.sql rename to example-mariadb/migrations/20240908062140_schema.sql diff --git a/example-mysql/src/main.rs b/example-mariadb/src/main.rs similarity index 100% rename from example-mysql/src/main.rs rename to example-mariadb/src/main.rs diff --git a/example-mysql/src/query2.rs b/example-mariadb/src/query2.rs similarity index 100% rename from example-mysql/src/query2.rs rename to example-mariadb/src/query2.rs diff --git a/scripts/mariadb.sh b/scripts/mariadb.sh index 223df8b..c01cb61 100755 --- a/scripts/mariadb.sh +++ b/scripts/mariadb.sh @@ -1,10 +1,7 @@ -CONTAINER_ID=$( - docker run -it --rm --name ormx-test-mariadb-db \ - -e MYSQL_DATABASE=ormx \ - -e MYSQL_ROOT_PASSWORD=admin \ - -v $(pwd)/scripts/mysql-schema.sql:/docker-entrypoint-initdb.d/schema.sql \ - -d mariadb -) -CONTAINER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $CONTAINER_ID) -echo "DATABASE_URL=mysql://root:admin@$CONTAINER_IP/ormx" > .env -docker attach $CONTAINER_ID +echo "DATABASE_URL=mysql://root:admin@127.0.0.1/ormx" > .env + +docker run -it --rm --name ormx-test-mariadb-db \ + -e MYSQL_DATABASE=ormx \ + -e MYSQL_ROOT_PASSWORD=admin \ + -p 3306:3306 \ + mariadb:latest \ No newline at end of file diff --git a/scripts/postgres.sh b/scripts/postgres.sh index a3d3358..f4f728f 100755 --- a/scripts/postgres.sh +++ b/scripts/postgres.sh @@ -1,11 +1,9 @@ -CONTAINER_ID=$( - docker run -it --rm --name ormx-test-postgres-db \ - -e POSTGRES_DB=ormx \ - -e POSTGRES_USER=postgres \ - -e POSTGRES_PASSWORD=admin \ - -v $(pwd)/scripts/postgres-schema.sql:/docker-entrypoint-initdb.d/schema.sql \ - -d postgres -) -CONTAINER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $CONTAINER_ID) -echo "DATABASE_URL=postgres://postgres:admin@$CONTAINER_IP/ormx" > .env -docker attach $CONTAINER_ID \ No newline at end of file +echo "DATABASE_URL=postgres://postgres:admin@127.0.0.1/ormx" > .env + +docker run -it --rm --name ormx-test-postgres-db \ + -e POSTGRES_DB=ormx \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=admin \ + -p 5432:5432 \ + postgres:latest \ + -c log_statement=all \ No newline at end of file From b41f8af7cce578053516e62d9c3379a03fe261eb Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 09:08:45 +0200 Subject: [PATCH 12/19] explicit (though not pretty) mysql support --- .github/workflows/examples.yml | 37 ++++- Cargo.toml | 2 +- README.md | 6 +- example-mariadb/Cargo.toml | 4 +- example-mysql/Cargo.toml | 19 +++ .../migrations/20240908062140_schema.sql | 0 example-mysql/src/main.rs | 91 +++++++++++ example-mysql/src/query2.rs | 27 ++++ ormx-macros/Cargo.toml | 1 + ormx-macros/src/backend/mariadb/insert.rs | 84 ++++++++++ ormx-macros/src/backend/mariadb/mod.rs | 37 +++++ ormx-macros/src/backend/mod.rs | 4 + ormx-macros/src/backend/mysql/insert.rs | 152 ++++++++++++------ ormx/Cargo.toml | 1 + ormx/src/lib.rs | 6 +- scripts/mysql.sh | 7 + scripts/postgres-schema.sql | 17 -- 17 files changed, 418 insertions(+), 77 deletions(-) create mode 100644 example-mysql/Cargo.toml rename scripts/mysql-schema.sql => example-mysql/migrations/20240908062140_schema.sql (100%) create mode 100644 example-mysql/src/main.rs create mode 100644 example-mysql/src/query2.rs create mode 100644 ormx-macros/src/backend/mariadb/insert.rs create mode 100644 ormx-macros/src/backend/mariadb/mod.rs create mode 100755 scripts/mysql.sh delete mode 100644 scripts/postgres-schema.sql diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 99c4cc1..8ae6799 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -8,6 +8,41 @@ on: - '*-dev' jobs: + mysql: + name: MySQL Example + runs-on: ubuntu-latest + + services: + mysql: + image: mysql:latest + env: + MYSQL_ROOT_PASSWORD: password + ports: + - 3306:3306 + + steps: + - uses: actions/checkout@v4 + + - uses: Swatinem/rust-cache@v2 + with: + key: mysql-example + + - name: Install sqlx-cli + run: cargo install sqlx-cli + + - name: Setup + working-directory: example-mysql + env: + DATABASE_URL: mysql://root:password@localhost:3306/ormx?ssl-mode=disabled + run: sqlx db setup + + - name: Run + working-directory: example-mysql + env: + DATABASE_URL: mysql://root:password@localhost:3306/ormx?ssl-mode=disabled + run: cargo run + + mariadb: name: MariaDB Example runs-on: ubuntu-latest @@ -25,7 +60,7 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - key: mariadb-examples + key: mariadb-example - name: Install sqlx-cli run: cargo install sqlx-cli diff --git a/Cargo.toml b/Cargo.toml index 17ad93d..fcb590c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["ormx-macros", "ormx", "example-postgres", "example-mariadb"] +members = ["ormx-macros", "ormx", "example-postgres", "example-mariadb", "example-mysql"] resolver = "2" \ No newline at end of file diff --git a/README.md b/README.md index dc40fcf..75117e6 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,8 @@ version = "0.8" default-features = false features = ["macros", "mysql", "runtime-tokio-rustls"] ``` -Right now, ormx supports mysql/mariadb and postgres. +Right now, ormx supports mysql, mariadb and postgres. + ## what does it do? ormx provides macros for generating commonly used sql queries at compile time. ormx is meant to be used together with sqlx. Everything it generates uses `sqlx::query!` under the hood, so every generated query will be checked against your database at compile time. @@ -57,8 +58,9 @@ is not what you are looking for. if you encounter an issue or have questions, feel free to ask in [`#ormx` on the sqlx discord](https://discord.gg/mrZz4Wv8r2). The documentation currently is not what it should be, so don't be afraid to ask for help. -## [mysql example](https://github.com/NyxCode/ormx/tree/master/example-mysql/src/main.rs) ## [postgres example](https://github.com/NyxCode/ormx/tree/master/example-postgres/src/main.rs) +## [mariadb example](https://github.com/NyxCode/ormx/tree/master/example-mariadb/src/main.rs) +## [mysql example](https://github.com/NyxCode/ormx/tree/master/example-mysql/src/main.rs) ## features - `mysql` - enable support for mysql/mariadb - `postgres` - enable support for postgres diff --git a/example-mariadb/Cargo.toml b/example-mariadb/Cargo.toml index ab8bf34..6f56c2c 100644 --- a/example-mariadb/Cargo.toml +++ b/example-mariadb/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "example-mysql" +name = "example-mariadb" version = "0.1.0" authors = ["moritz"] edition = "2021" [dependencies] -ormx = { path = "../ormx", features = ["mysql"] } +ormx = { path = "../ormx", features = ["mariadb"] } tokio = { version = "1", features = ["full"] } anyhow = "1" dotenv = "0.15" diff --git a/example-mysql/Cargo.toml b/example-mysql/Cargo.toml new file mode 100644 index 0000000..ab8bf34 --- /dev/null +++ b/example-mysql/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "example-mysql" +version = "0.1.0" +authors = ["moritz"] +edition = "2021" + +[dependencies] +ormx = { path = "../ormx", features = ["mysql"] } +tokio = { version = "1", features = ["full"] } +anyhow = "1" +dotenv = "0.15" +chrono = "0.4" +env_logger = "0.11.5" +log = "0.4" + +[dependencies.sqlx] +version = "0.8" +default-features = false +features = ["macros", "mysql", "runtime-tokio-rustls", "chrono"] diff --git a/scripts/mysql-schema.sql b/example-mysql/migrations/20240908062140_schema.sql similarity index 100% rename from scripts/mysql-schema.sql rename to example-mysql/migrations/20240908062140_schema.sql diff --git a/example-mysql/src/main.rs b/example-mysql/src/main.rs new file mode 100644 index 0000000..26ba842 --- /dev/null +++ b/example-mysql/src/main.rs @@ -0,0 +1,91 @@ +use chrono::{NaiveDateTime, Utc}; +use log::LevelFilter; +use ormx::{Delete, Insert, Table}; +use sqlx::MySqlPool; + +// To run this example, first run `/scripts/mariadb.sh` to start mariadb in a docker container and +// write the database URL to `.env`. Then, source `.env` (`. .env`) and run `cargo run` + +mod query2; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + dotenv::dotenv().ok(); + env_logger::builder() + .filter_level(LevelFilter::Debug) + .init(); + + let db = MySqlPool::connect(&dotenv::var("DATABASE_URL")?).await?; + + log::info!("insert a new row into the database"); + let mut new = InsertUser { + first_name: "Moritz".to_owned(), + last_name: "Bischof".to_owned(), + email: "moritz.bischof1@gmail.com".to_owned(), + disabled: None, + } + .insert(&mut *db.acquire().await?) + .await?; + + log::info!("update a single field"); + new.set_last_login(&db, Some(Utc::now().naive_utc())) + .await?; + + log::info!("update all fields at once"); + new.email = "asdf".to_owned(); + new.update(&db).await?; + + log::info!("apply a patch to the user, updating its first and last name"); + new.patch( + &db, + UpdateName { + first_name: "NewFirstName".to_owned(), + last_name: "NewLastName".to_owned(), + disabled: Some("Reason".to_owned()), + }, + ) + .await?; + + log::info!("reload the user, in case it has been modified"); + new.reload(&db).await?; + + log::info!("use the improved query macro for searching users"); + let search_result = query2::query_users(&db, Some("NewFirstName"), None).await?; + println!("{:?}", search_result); + + log::info!("delete the user from the database"); + new.delete(&db).await?; + + Ok(()) +} + +#[derive(Debug, ormx::Table)] +#[ormx(table = "users", id = user_id, insertable, deletable)] +struct User { + // map this field to the column "id" + #[ormx(column = "id")] + // generate `User::get_by_user_id(u32) -> Result` + #[ormx(get_one = get_by_user_id(u32))] + // this column is database-generated. + #[ormx(default)] + user_id: u32, + first_name: String, + last_name: String, + // generate `User::by_email(&str) -> Result>` + #[ormx(get_optional(&str))] + email: String, + disabled: Option, + // don't include this field into `InsertUser` since it has a default value + // generate `User::set_last_login(Option) -> Result<()>` + #[ormx(default, set)] + last_login: Option, +} + +// Patches can be used to update multiple fields at once (in diesel, they're called "ChangeSets"). +#[derive(ormx::Patch)] +#[ormx(table_name = "users", table = crate::User, id = "id")] +struct UpdateName { + first_name: String, + last_name: String, + disabled: Option, +} diff --git a/example-mysql/src/query2.rs b/example-mysql/src/query2.rs new file mode 100644 index 0000000..fe658ae --- /dev/null +++ b/example-mysql/src/query2.rs @@ -0,0 +1,27 @@ +use sqlx::MySqlPool; + +use crate::User; + +pub(crate) async fn query_users( + db: &MySqlPool, + filter: Option<&str>, + limit: Option, +) -> anyhow::Result> { + let result = ormx::conditional_query_as!( + User, + r#"SELECT id AS user_id, first_name, last_name, email, disabled, last_login"# + "FROM users" + Some(f) = filter => { + "WHERE first_name LIKE" ?(f) + "OR last_name LIKE" ?(f) + } + "ORDER BY first_name DESC" + Some(l) = limit => { + "LIMIT" ?(l as i64) + } + ) + .fetch_all(db) + .await?; + + Ok(result) +} diff --git a/ormx-macros/Cargo.toml b/ormx-macros/Cargo.toml index ff29fcd..a5c97e8 100644 --- a/ormx-macros/Cargo.toml +++ b/ormx-macros/Cargo.toml @@ -13,6 +13,7 @@ proc-macro = true [features] sqlite = [] mysql = [] +mariadb = [] postgres = [] [dependencies] diff --git a/ormx-macros/src/backend/mariadb/insert.rs b/ormx-macros/src/backend/mariadb/insert.rs new file mode 100644 index 0000000..bd54a0d --- /dev/null +++ b/ormx-macros/src/backend/mariadb/insert.rs @@ -0,0 +1,84 @@ +use itertools::Itertools; +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::Ident; + +use crate::{ + backend::mariadb::{MariaBackend, MariaBindings}, + table::{Table, TableField}, +}; + +pub fn impl_insert(table: &Table) -> TokenStream { + let insert_ident = match &table.insertable { + Some(i) => &i.ident, + None => return quote!(), + }; + + let insert_fields: Vec<&TableField> = table.insertable_fields().collect(); + let default_fields: Vec<&TableField> = table.default_fields().collect(); + + let table_ident = &table.ident; + let insert_field_idents = insert_fields.iter().map(|field| &field.field); + let default_field_idents = default_fields.iter().map(|field| &field.field); + let default_field_ordinals = (0usize..).take(default_fields.len()); + + let insert_sql = insert_sql(table, &insert_fields); + + let insert_field_exprs = insert_fields.iter().map(|f| f.fmt_as_argument()); + + let fetch_fn = if default_fields.is_empty() { + Ident::new("execute", Span::call_site()) + } else { + Ident::new("fetch_one", Span::call_site()) + }; + + quote! { + impl ormx::Insert for #insert_ident { + type Table = #table_ident; + + async fn insert<'a, 'c: 'a>( + self, + db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a, + ) -> sqlx::Result { + use sqlx::Row; + + let _generated = sqlx::query!(#insert_sql, #( #insert_field_exprs, )*) + .#fetch_fn(db) + .await?; + + Ok(Self::Table { + #( #insert_field_idents: self.#insert_field_idents, )* + #( #default_field_idents: _generated.get(#default_field_ordinals), )* + }) + } + } + } +} + +fn insert_sql(table: &Table, insert_fields: &[&TableField]) -> String { + let columns = insert_fields.iter().map(|field| field.column()).join(", "); + let fields = MariaBindings::default() + .take(insert_fields.len()) + .join(", "); + let returning_fields = table + .default_fields() + .map(TableField::fmt_for_select) + .join(", "); + + if returning_fields.is_empty() { + format!( + "INSERT INTO {} ({}) VALUES ({})", + table.name(), + columns, + fields + ) + } else { + format!( + "INSERT INTO {} ({}) VALUES ({}) RETURNING {}", + table.name(), + columns, + fields, + returning_fields + ) + } +} diff --git a/ormx-macros/src/backend/mariadb/mod.rs b/ormx-macros/src/backend/mariadb/mod.rs new file mode 100644 index 0000000..938fa84 --- /dev/null +++ b/ormx-macros/src/backend/mariadb/mod.rs @@ -0,0 +1,37 @@ +use std::borrow::Cow; + +use proc_macro2::TokenStream; +use quote::quote; + +use crate::{backend::Backend, table::Table}; + +mod insert; + +#[derive(Clone)] +pub struct MariaBackend; + +impl Backend for MariaBackend { + const QUOTE: char = '`'; + const IS_MYSQL: bool = true; + + type Bindings = MariaBindings; + + fn query_result() -> TokenStream { + quote!(sqlx::mysql::MySqlQueryResult) + } + + fn impl_insert(table: &Table) -> TokenStream { + insert::impl_insert(table) + } +} + +#[derive(Default)] +pub struct MariaBindings; + +impl Iterator for MariaBindings { + type Item = Cow<'static, str>; + + fn next(&mut self) -> Option { + Some(Cow::Borrowed("?")) + } +} diff --git a/ormx-macros/src/backend/mod.rs b/ormx-macros/src/backend/mod.rs index 9bde9df..3d5a476 100644 --- a/ormx-macros/src/backend/mod.rs +++ b/ormx-macros/src/backend/mod.rs @@ -5,11 +5,15 @@ use proc_macro2::TokenStream; use crate::{patch::Patch, table::Table}; mod common; +#[cfg(feature = "mariadb")] +mod mariadb; #[cfg(feature = "mysql")] mod mysql; #[cfg(feature = "postgres")] mod postgres; +#[cfg(feature = "mariadb")] +pub type Implementation = mariadb::MariaBackend; #[cfg(feature = "mysql")] pub type Implementation = mysql::MySqlBackend; #[cfg(feature = "postgres")] diff --git a/ormx-macros/src/backend/mysql/insert.rs b/ormx-macros/src/backend/mysql/insert.rs index f2c194c..772ae2b 100644 --- a/ormx-macros/src/backend/mysql/insert.rs +++ b/ormx-macros/src/backend/mysql/insert.rs @@ -1,10 +1,10 @@ use itertools::Itertools; -use proc_macro2::{Span, TokenStream}; +use proc_macro2::TokenStream; use quote::quote; -use syn::Ident; +use super::MySqlBackend; use crate::{ - backend::mysql::{MySqlBackend, MySqlBindings}, + backend::mysql::MySqlBindings, table::{Table, TableField}, }; @@ -14,23 +14,12 @@ pub fn impl_insert(table: &Table) -> TokenStream { None => return quote!(), }; - let insert_fields: Vec<&TableField> = table.insertable_fields().collect(); - let default_fields: Vec<&TableField> = table.default_fields().collect(); - let table_ident = &table.ident; - let insert_field_idents = insert_fields.iter().map(|field| &field.field); - let default_field_idents = default_fields.iter().map(|field| &field.field); - let default_field_ordinals = (0usize..).take(default_fields.len()); - - let insert_sql = insert_sql(table, &insert_fields); - - let insert_field_exprs = insert_fields.iter().map(|f| f.fmt_as_argument()); - let fetch_fn = if default_fields.is_empty() { - Ident::new("execute", Span::call_site()) - } else { - Ident::new("fetch_one", Span::call_site()) - }; + let insert = insert(table); + let query_id = query_id(table); + let query_default = query_default(table); + let construct_row = construct_row(table); quote! { impl ormx::Insert for #insert_ident { @@ -38,47 +27,106 @@ pub fn impl_insert(table: &Table) -> TokenStream { async fn insert<'a, 'c: 'a>( self, - db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a, + db: &'c mut sqlx::MySqlConnection, ) -> sqlx::Result { - use sqlx::Row; - - let _generated = sqlx::query!(#insert_sql, #( #insert_field_exprs, )*) - .#fetch_fn(db) - .await?; + use sqlx::Connection; - Ok(Self::Table { - #( #insert_field_idents: self.#insert_field_idents, )* - #( #default_field_idents: _generated.get(#default_field_ordinals), )* - }) + let mut tx = db.begin().await?; + #insert + #query_id + #query_default + tx.commit().await?; + Ok(#construct_row) } } } } -fn insert_sql(table: &Table, insert_fields: &[&TableField]) -> String { - let columns = insert_fields.iter().map(|field| field.column()).join(", "); - let fields = MySqlBindings::default() - .take(insert_fields.len()) - .join(", "); - let returning_fields = table +/// build an instance of the table struct from +/// - `_id` (see `query_id` below) +/// - `_generated` (see `query_default` below) +/// - all fields already present in the insert struct +fn construct_row(table: &Table) -> TokenStream { + let id_ident = &table.id.field; + let insert_field_idents = table + .insertable_fields() + .map(|f| &f.field) + .filter(|f| *f != id_ident); + let default_field_idents = table .default_fields() - .map(TableField::fmt_for_select) - .join(", "); - - if returning_fields.is_empty() { - format!( - "INSERT INTO {} ({}) VALUES ({})", - table.name(), - columns, - fields - ) - } else { - format!( - "INSERT INTO {} ({}) VALUES ({}) RETURNING {}", - table.name(), - columns, - fields, - returning_fields - ) + .map(|f| &f.field) + .filter(|f| *f != id_ident); + + quote! { + Self::Table { + #id_ident: _id as _, + #( #insert_field_idents: self.#insert_field_idents, )* + #( #default_field_idents: _generated.#default_field_idents, )* + } + } +} + +/// queries default fields from the database, except the ID. +fn query_default(table: &Table) -> TokenStream { + let mut default_fields = table + .default_fields() + .filter(|f| f.field != table.id.field) + .peekable(); + + if default_fields.peek().is_none() { + return quote!(); + } + + let query_default_sql = format!( + "SELECT {} FROM {} WHERE {} = ?", + default_fields.map(TableField::fmt_for_select).join(", "), + table.name(), + table.id.column() + ); + + quote! { + let _generated = sqlx::query!(#query_default_sql, _id) + .fetch_one(&mut *tx) + .await?; + } +} + +/// inserts the struct into the database +fn insert(table: &Table) -> TokenStream { + let insert_fields: Vec<_> = table.insertable_fields().collect(); + let insert_field_idents = insert_fields.iter().map(|field| &field.field); + + let insert_sql = format!( + "INSERT INTO {} ({}) VALUES ({})", + table.name(), + insert_fields.iter().map(|field| field.column()).join(", "), + MySqlBindings.take(insert_fields.len()).join(", ") + ); + + quote! { + sqlx::query!(#insert_sql, #( self.#insert_field_idents, )*) + .execute(&mut *tx) + .await?; + } +} + +/// obtains the id of the inserted row. +/// +/// case 1: +/// The ID is database generated, so we query it with LAST_INSERT_ID +/// case 2: +/// The ID is already known, so we can just use it. +fn query_id(table: &Table) -> TokenStream { + match table.id.default { + true => quote! { + let _id = sqlx::query!("SELECT LAST_INSERT_ID() AS id") + .fetch_one(&mut *tx) + .await? + .id; + }, + false => { + let id_ident = &table.id.field; + quote!(let _id = self.#id_ident;) + } } } diff --git a/ormx/Cargo.toml b/ormx/Cargo.toml index dd25a89..9473c4b 100644 --- a/ormx/Cargo.toml +++ b/ormx/Cargo.toml @@ -14,6 +14,7 @@ targets = ["x86_64-unknown-linux-gnu"] [features] mysql = ["sqlx/mysql", "ormx-macros/mysql"] +mariadb = ["sqlx/mysql", "ormx-macros/mariadb"] sqlite = ["sqlx/sqlite", "ormx-macros/sqlite"] postgres = ["sqlx/postgres", "ormx-macros/postgres"] diff --git a/ormx/src/lib.rs b/ormx/src/lib.rs index ff4e399..5c4a3e6 100644 --- a/ormx/src/lib.rs +++ b/ormx/src/lib.rs @@ -66,7 +66,8 @@ where /// Insert a row into the database. fn insert<'a, 'c: 'a>( - db: impl Executor<'c, Database = Db> + 'a, + #[cfg(not(feature = "mysql"))] db: impl Executor<'c, Database = Db> + 'a, + #[cfg(feature = "mysql")] db: &'c mut sqlx::MySqlConnection, row: impl Insert
, ) -> impl Future> + Send + 'a { row.insert(db) @@ -195,7 +196,8 @@ where /// Insert a row into the database, returning the inserted row. fn insert<'a, 'c: 'a>( self, - db: impl Executor<'c, Database = Db> + 'a, + #[cfg(not(feature = "mysql"))] db: impl Executor<'c, Database = Db> + 'a, + #[cfg(feature = "mysql")] db: &'c mut sqlx::MySqlConnection, ) -> impl Future> + Send + 'a; } diff --git a/scripts/mysql.sh b/scripts/mysql.sh new file mode 100755 index 0000000..64ca699 --- /dev/null +++ b/scripts/mysql.sh @@ -0,0 +1,7 @@ +echo "DATABASE_URL=mysql://root:admin@127.0.0.1/ormx" > .env + +docker run -it --rm --name ormx-test-mariadb-db \ + -e MYSQL_DATABASE=ormx \ + -e MYSQL_ROOT_PASSWORD=admin \ + -p 3306:3306 \ + mysql:latest \ No newline at end of file diff --git a/scripts/postgres-schema.sql b/scripts/postgres-schema.sql deleted file mode 100644 index 192c64b..0000000 --- a/scripts/postgres-schema.sql +++ /dev/null @@ -1,17 +0,0 @@ -CREATE TYPE user_role AS ENUM ('user', 'admin'); - -CREATE TABLE users -( - id SERIAL PRIMARY KEY, - first_name VARCHAR(128) NOT NULL, - last_name VARCHAR(128) NOT NULL, - email VARCHAR(128) NOT NULL UNIQUE, - role user_role NOT NULL, - disabled TEXT, - last_login TIMESTAMP DEFAULT NULL -); - -CREATE TABLE test ( - id SERIAL PRIMARY KEY, - rows TEXT[] NOT NULL -); \ No newline at end of file From 414896d170b9644b207d7af94c5c481b291df861 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 09:15:50 +0200 Subject: [PATCH 13/19] fix feature flags --- ormx/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ormx/src/lib.rs b/ormx/src/lib.rs index 5c4a3e6..bef86ef 100644 --- a/ormx/src/lib.rs +++ b/ormx/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))] +#![cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite", feature = "mariadb"))] //! Lightweight derive macros for bringing orm-like features to sqlx. //! //! # Example: Table @@ -43,10 +43,10 @@ pub mod exports { pub use crate::query2::map::*; } -#[cfg(any(feature = "mysql", feature = "postgres"))] +#[cfg(any(feature = "mysql", feature = "postgres", feature = "mariadb"))] mod query2; -#[cfg(feature = "mysql")] +#[cfg(any(feature = "mysql", feature = "mariadb"))] pub type Db = sqlx::MySql; #[cfg(feature = "postgres")] pub type Db = sqlx::Postgres; From 54aa6cd1a689ae3ad50055b20b4a2c6a661e98ca Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 09:23:49 +0200 Subject: [PATCH 14/19] fix feature flags --- .github/workflows/examples.yml | 12 +++++++++--- ormx-macros/src/lib.rs | 2 +- ormx/src/query2/mod.rs | 2 +- scripts/mysql.sh | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 8ae6799..d4be4f8 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -28,7 +28,9 @@ jobs: key: mysql-example - name: Install sqlx-cli - run: cargo install sqlx-cli + run: | + cargo install sqlx-cli + sleep 3 - name: Setup working-directory: example-mysql @@ -63,7 +65,9 @@ jobs: key: mariadb-example - name: Install sqlx-cli - run: cargo install sqlx-cli + run: | + cargo install sqlx-cli + sleep 3 - name: Setup working-directory: example-mariadb @@ -97,7 +101,9 @@ jobs: key: postgres-example - name: Install sqlx-cli - run: cargo install sqlx-cli + run: | + cargo install sqlx-cli + sleep 3 - name: Setup working-directory: example-postgres diff --git a/ormx-macros/src/lib.rs b/ormx-macros/src/lib.rs index 19852b4..f7e73ed 100644 --- a/ormx-macros/src/lib.rs +++ b/ormx-macros/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))] +#![cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite", feature = "mariadb"))] mod attrs; mod backend; diff --git a/ormx/src/query2/mod.rs b/ormx/src/query2/mod.rs index 0b0cf65..63c20ab 100644 --- a/ormx/src/query2/mod.rs +++ b/ormx/src/query2/mod.rs @@ -1,7 +1,7 @@ #[doc(hidden)] #[rustfmt::skip] pub mod map; -#[cfg(feature = "mysql")] +#[cfg(any(feature = "mysql", feature = "mariadb"))] mod mysql; #[cfg(feature = "postgres")] mod postgres; diff --git a/scripts/mysql.sh b/scripts/mysql.sh index 64ca699..16a1a48 100755 --- a/scripts/mysql.sh +++ b/scripts/mysql.sh @@ -1,6 +1,6 @@ echo "DATABASE_URL=mysql://root:admin@127.0.0.1/ormx" > .env -docker run -it --rm --name ormx-test-mariadb-db \ +docker run -it --rm --name ormx-test-mysql-db \ -e MYSQL_DATABASE=ormx \ -e MYSQL_ROOT_PASSWORD=admin \ -p 3306:3306 \ From 5483384b3c313333d3aa3556cdf46accc2a8a4b7 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 09:30:53 +0200 Subject: [PATCH 15/19] convince mysql to start --- .github/workflows/examples.yml | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index d4be4f8..6679113 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -17,8 +17,9 @@ jobs: image: mysql:latest env: MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: ormx ports: - - 3306:3306 + - 3307:3306 steps: - uses: actions/checkout@v4 @@ -28,20 +29,18 @@ jobs: key: mysql-example - name: Install sqlx-cli - run: | - cargo install sqlx-cli - sleep 3 + run: cargo install sqlx-cli - name: Setup working-directory: example-mysql env: - DATABASE_URL: mysql://root:password@localhost:3306/ormx?ssl-mode=disabled + DATABASE_URL: mysql://root:password@localhost:3307/ormx?ssl-mode=disabled run: sqlx db setup - name: Run working-directory: example-mysql env: - DATABASE_URL: mysql://root:password@localhost:3306/ormx?ssl-mode=disabled + DATABASE_URL: mysql://root:password@localhost:3307/ormx?ssl-mode=disabled run: cargo run @@ -65,9 +64,7 @@ jobs: key: mariadb-example - name: Install sqlx-cli - run: | - cargo install sqlx-cli - sleep 3 + run: cargo install sqlx-cli - name: Setup working-directory: example-mariadb @@ -101,9 +98,7 @@ jobs: key: postgres-example - name: Install sqlx-cli - run: | - cargo install sqlx-cli - sleep 3 + run: cargo install sqlx-cli - name: Setup working-directory: example-postgres From 32193f9358a0526275b12af501db63cac30dedbb Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 09:32:53 +0200 Subject: [PATCH 16/19] CI: add sleep for mysql & mariadb --- .github/workflows/examples.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 6679113..6dbe004 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -29,7 +29,9 @@ jobs: key: mysql-example - name: Install sqlx-cli - run: cargo install sqlx-cli + run: | + cargo install sqlx-cli + sleep 5 - name: Setup working-directory: example-mysql @@ -64,7 +66,9 @@ jobs: key: mariadb-example - name: Install sqlx-cli - run: cargo install sqlx-cli + run: | + cargo install sqlx-cli + sleep 5 - name: Setup working-directory: example-mariadb From 277eb0dc3eb56361a70e3de8acf982a38102b3ce Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 09:58:31 +0200 Subject: [PATCH 17/19] fix #24 --- .../migrations/20240908062042_schema.sql | 4 ++++ example-postgres/src/main.rs | 22 +++++++++++++++++++ example-postgres/src/query2.rs | 4 +++- ormx-macros/src/table/mod.rs | 4 ++-- ormx-macros/src/table/parse.rs | 6 ++--- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/example-postgres/migrations/20240908062042_schema.sql b/example-postgres/migrations/20240908062042_schema.sql index b9e0069..d6e805c 100644 --- a/example-postgres/migrations/20240908062042_schema.sql +++ b/example-postgres/migrations/20240908062042_schema.sql @@ -1,4 +1,6 @@ CREATE TYPE user_role AS ENUM ('user', 'admin'); +CREATE TYPE account_type AS ENUM ('legacy', 'normal'); +CREATE TYPE user_group AS ENUM ('local', 'global', 'other'); CREATE TABLE users ( @@ -7,6 +9,8 @@ CREATE TABLE users last_name VARCHAR(128) NOT NULL, email VARCHAR(128) NOT NULL UNIQUE, role user_role NOT NULL, + type account_type NOT NULL, + "group" user_group NOT NULL DEFAULT 'local', disabled TEXT, last_login TIMESTAMP DEFAULT NULL ); diff --git a/example-postgres/src/main.rs b/example-postgres/src/main.rs index 5d1125b..8c62374 100644 --- a/example-postgres/src/main.rs +++ b/example-postgres/src/main.rs @@ -25,6 +25,7 @@ async fn main() -> anyhow::Result<()> { email: "moritz.bischof1@gmail.com".to_owned(), disabled: None, role: Role::User, + ty: AccountType::Normal, } .insert(&db) .await?; @@ -76,6 +77,10 @@ struct User { email: String, #[ormx(custom_type)] role: Role, + #[ormx(column = "type", custom_type)] + ty: AccountType, + #[ormx(custom_type, default, set)] + group: UserGroup, disabled: Option, // don't include this field into `InsertUser` since it has a default value // generate `User::set_last_login(Option) -> Result<()>` @@ -102,6 +107,23 @@ enum Role { Admin, } +#[derive(Debug, Copy, Clone, sqlx::Type)] +#[sqlx(type_name = "account_type")] +#[sqlx(rename_all = "lowercase")] +enum AccountType { + Legacy, + Normal, +} + +#[derive(Debug, Copy, Clone, sqlx::Type)] +#[sqlx(type_name = "user_group")] +#[sqlx(rename_all = "lowercase")] +enum UserGroup { + Local, + Global, + Other, +} + #[derive(Debug, ormx::Table)] #[ormx(table = "test", id = id, insertable)] struct Test { diff --git a/example-postgres/src/query2.rs b/example-postgres/src/query2.rs index 66092ed..bf40290 100644 --- a/example-postgres/src/query2.rs +++ b/example-postgres/src/query2.rs @@ -9,7 +9,9 @@ pub(crate) async fn query_users( ) -> anyhow::Result> { let result = ormx::conditional_query_as!( User, - r#"SELECT id AS user_id, first_name, last_name, email, disabled, role AS "role: _", last_login"# + r#"SELECT + id AS user_id, first_name, last_name, email, disabled, + role AS "role: _", "group" AS "group: _", type as "ty: _", last_login"# "FROM users" Some(f) = filter => { "WHERE first_name LIKE" ?(f) diff --git a/ormx-macros/src/table/mod.rs b/ormx-macros/src/table/mod.rs index 91c0e4e..7e11657 100644 --- a/ormx-macros/src/table/mod.rs +++ b/ormx-macros/src/table/mod.rs @@ -2,7 +2,7 @@ use std::{convert::TryFrom, marker::PhantomData}; use itertools::Itertools; use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; +use quote::{quote, ToTokens}; use syn::{Attribute, DeriveInput, Result, Type, Visibility}; use crate::{ @@ -70,7 +70,7 @@ impl TableField { let q = B::QUOTE; if self.custom_type { - format!("{q}{}{q} AS {q}{}: _{q}", self.column_name, self.field,) + format!("{q}{}{q} AS {q}{}: {}{q}", self.column_name, self.field.to_string(), self.ty.to_token_stream()) } else if self.field == self.column_name { self.column() } else { diff --git a/ormx-macros/src/table/parse.rs b/ormx-macros/src/table/parse.rs index 48a8b63..e046d6e 100644 --- a/ormx-macros/src/table/parse.rs +++ b/ormx-macros/src/table/parse.rs @@ -2,7 +2,7 @@ use std::{convert::TryFrom, marker::PhantomData}; use proc_macro2::Span; use syn::{Data, DeriveInput, Error, Ident, Result}; - +use syn::ext::IdentExt; use super::{Table, TableField}; use crate::{ attrs::{parse_attrs, Insertable, TableAttr, TableFieldAttr}, @@ -40,7 +40,7 @@ impl TryFrom<&syn::Field> for TableField { TableFieldAttr::GetOptional(g) => set_once(&mut get_optional, g)?, TableFieldAttr::GetMany(g) => set_once(&mut get_many, g)?, TableFieldAttr::Set(s) => { - let default = || Ident::new(&format!("set_{}", ident), Span::call_site()); + let default = || Ident::new(&format!("set_{}", ident.unraw()), Span::call_site()); set_once(&mut set, s.unwrap_or_else(default))? } TableFieldAttr::Default(..) => set_once(&mut default, true)?, @@ -49,7 +49,7 @@ impl TryFrom<&syn::Field> for TableField { } } Ok(TableField { - column_name: column.unwrap_or_else(|| ident.to_string()), + column_name: column.unwrap_or_else(|| ident.unraw().to_string()), field: ident, ty: value.ty.clone(), custom_type: custom_type.unwrap_or(false), From 3771d9aa77b06e893ee067d9f38b75f375410697 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 10:22:31 +0200 Subject: [PATCH 18/19] implement #17 --- example-postgres/src/main.rs | 8 ++++++-- ormx-macros/src/attrs.rs | 5 ++++- ormx-macros/src/backend/common/table.rs | 16 ++++++++++++++-- ormx-macros/src/lib.rs | 7 ++++++- ormx-macros/src/table/mod.rs | 8 +++++++- ormx-macros/src/table/parse.rs | 11 +++++++---- ormx/src/lib.rs | 17 ++++++++++++++++- 7 files changed, 60 insertions(+), 12 deletions(-) diff --git a/example-postgres/src/main.rs b/example-postgres/src/main.rs index 8c62374..5ab85e8 100644 --- a/example-postgres/src/main.rs +++ b/example-postgres/src/main.rs @@ -55,7 +55,11 @@ async fn main() -> anyhow::Result<()> { log::info!("use the improved query macro for searching users"); let search_result = query2::query_users(&db, Some("NewFirstName"), None).await?; - println!("{:?}", search_result); + log::info!("search result: {:?}", search_result); + + log::info!("load all users in the order specified by the 'order_by' attribute"); + let all = User::all_paginated(&db, 0, 100).await?; + log::info!("all users: {all:?}"); log::info!("delete the user from the database"); new.delete(&db).await?; @@ -64,7 +68,7 @@ async fn main() -> anyhow::Result<()> { } #[derive(Debug, ormx::Table)] -#[ormx(table = "users", id = user_id, insertable, deletable)] +#[ormx(table = "users", id = user_id, insertable, deletable, order_by = "email ASC")] struct User { // map this field to the column "id" #[ormx(column = "id")] diff --git a/ormx-macros/src/attrs.rs b/ormx-macros/src/attrs.rs index 3d90461..f34626e 100644 --- a/ormx-macros/src/attrs.rs +++ b/ormx-macros/src/attrs.rs @@ -13,6 +13,8 @@ pub enum TableAttr { Insertable(Option), // deletable Deletable(()), + // order_by = + OrderBy(String), } pub struct Insertable { @@ -144,7 +146,8 @@ impl_parse!(TableAttr { "table" => Table(= String), "id" => Id(= Ident), "insertable" => Insertable((= Insertable)?), - "deletable" => Deletable() + "deletable" => Deletable(), + "order_by" => OrderBy(= String) }); impl_parse!(TableFieldAttr { diff --git a/ormx-macros/src/backend/common/table.rs b/ormx-macros/src/backend/common/table.rs index a674aad..e64abac 100644 --- a/ormx-macros/src/backend/common/table.rs +++ b/ormx-macros/src/backend/common/table.rs @@ -101,7 +101,15 @@ fn update(table: &Table) -> TokenStream { fn stream_all(table: &Table, column_list: &str) -> TokenStream { let return_type = crate::utils::stream!(sqlx::Result); - let all_sql = format!("SELECT {} FROM {}", column_list, table.name()); + let order_by = match &table.order_by { + None => &format!("{} DESC", table.id.column()), + Some(by) => by, + }; + let all_sql = format!( + "SELECT {} FROM {} ORDER BY {order_by}", + column_list, + table.name() + ); quote! { fn stream_all<'a, 'c: 'a>( @@ -116,8 +124,12 @@ fn stream_all(table: &Table, column_list: &str) -> TokenStream { fn stream_all_paginated(table: &Table, column_list: &str) -> TokenStream { let return_type = crate::utils::stream!(sqlx::Result); let mut bindings = B::Bindings::default(); + let order_by = match &table.order_by { + None => &format!("{} DESC", table.id.column()), + Some(by) => by, + }; let all_sql = format!( - "SELECT {} FROM {} LIMIT {} OFFSET {}", + "SELECT {} FROM {} ORDER BY {order_by} LIMIT {} OFFSET {} ", column_list, table.name(), bindings.next().unwrap(), diff --git a/ormx-macros/src/lib.rs b/ormx-macros/src/lib.rs index f7e73ed..511f86b 100644 --- a/ormx-macros/src/lib.rs +++ b/ormx-macros/src/lib.rs @@ -1,4 +1,9 @@ -#![cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite", feature = "mariadb"))] +#![cfg(any( + feature = "mysql", + feature = "postgres", + feature = "sqlite", + feature = "mariadb" +))] mod attrs; mod backend; diff --git a/ormx-macros/src/table/mod.rs b/ormx-macros/src/table/mod.rs index 7e11657..9224322 100644 --- a/ormx-macros/src/table/mod.rs +++ b/ormx-macros/src/table/mod.rs @@ -20,6 +20,7 @@ pub struct Table { pub fields: Vec>, pub insertable: Option, pub deletable: bool, + pub order_by: Option, } #[derive(Clone)] @@ -70,7 +71,12 @@ impl TableField { let q = B::QUOTE; if self.custom_type { - format!("{q}{}{q} AS {q}{}: {}{q}", self.column_name, self.field.to_string(), self.ty.to_token_stream()) + format!( + "{q}{}{q} AS {q}{}: {}{q}", + self.column_name, + self.field.to_string(), + self.ty.to_token_stream() + ) } else if self.field == self.column_name { self.column() } else { diff --git a/ormx-macros/src/table/parse.rs b/ormx-macros/src/table/parse.rs index e046d6e..540c78f 100644 --- a/ormx-macros/src/table/parse.rs +++ b/ormx-macros/src/table/parse.rs @@ -1,8 +1,8 @@ use std::{convert::TryFrom, marker::PhantomData}; use proc_macro2::Span; -use syn::{Data, DeriveInput, Error, Ident, Result}; -use syn::ext::IdentExt; +use syn::{ext::IdentExt, Data, DeriveInput, Error, Ident, Result}; + use super::{Table, TableField}; use crate::{ attrs::{parse_attrs, Insertable, TableAttr, TableFieldAttr}, @@ -40,7 +40,8 @@ impl TryFrom<&syn::Field> for TableField { TableFieldAttr::GetOptional(g) => set_once(&mut get_optional, g)?, TableFieldAttr::GetMany(g) => set_once(&mut get_many, g)?, TableFieldAttr::Set(s) => { - let default = || Ident::new(&format!("set_{}", ident.unraw()), Span::call_site()); + let default = + || Ident::new(&format!("set_{}", ident.unraw()), Span::call_site()); set_once(&mut set, s.unwrap_or_else(default))? } TableFieldAttr::Default(..) => set_once(&mut default, true)?, @@ -80,7 +81,7 @@ impl TryFrom<&DeriveInput> for Table { .map(TableField::try_from) .collect::>>()?; - none!(table, id, insertable, deletable); + none!(table, id, insertable, deletable, order_by); for attr in parse_attrs::(&value.attrs)? { match attr { TableAttr::Table(x) => set_once(&mut table, x)?, @@ -93,6 +94,7 @@ impl TryFrom<&DeriveInput> for Table { set_once(&mut insertable, x.unwrap_or_else(default))?; } TableAttr::Deletable(_) => set_once(&mut deletable, true)?, + TableAttr::OrderBy(by) => set_once(&mut order_by, by)?, } } @@ -123,6 +125,7 @@ impl TryFrom<&DeriveInput> for Table { insertable, fields, deletable: deletable.unwrap_or(false), + order_by, }) } } diff --git a/ormx/src/lib.rs b/ormx/src/lib.rs index bef86ef..19471cb 100644 --- a/ormx/src/lib.rs +++ b/ormx/src/lib.rs @@ -1,4 +1,9 @@ -#![cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite", feature = "mariadb"))] +#![cfg(any( + feature = "mysql", + feature = "postgres", + feature = "sqlite", + feature = "mariadb" +))] //! Lightweight derive macros for bringing orm-like features to sqlx. //! //! # Example: Table @@ -80,10 +85,15 @@ where ) -> impl Future> + Send + 'a; /// Stream all rows from this table. + /// By default, results are ordered in descending order according to their ID column. + /// This can be configured using `#[ormx(order_by = "some_column ASC")]`. fn stream_all<'a, 'c: 'a>( db: impl Executor<'c, Database = Db> + 'a, ) -> impl Stream> + Send + 'a; + /// Streams at most `limit` rows from this table, skipping the first `offset` rows. + /// By default, results are ordered in descending order according to their ID column. + /// This can be configured using `#[ormx(order_by = "some_column ASC")]`. fn stream_all_paginated<'a, 'c: 'a>( db: impl Executor<'c, Database = Db> + 'a, offset: i64, @@ -91,12 +101,17 @@ where ) -> impl Stream> + Send + 'a; /// Load all rows from this table. + /// By default, results are ordered in descending order according to their ID column. + /// This can be configured using `#[ormx(order_by = "some_column ASC")]`. fn all<'a, 'c: 'a>( db: impl Executor<'c, Database = Db> + 'a, ) -> impl Future>> + Send + 'a { Self::stream_all(db).try_collect() } + /// Load at most `limit` rows from this table, skipping the first `offset`. + /// By default, results are ordered in descending order according to their ID column. + /// This can be configured using `#[ormx(order_by = "some_column ASC")]`. fn all_paginated<'a, 'c: 'a>( db: impl Executor<'c, Database = Db> + 'a, offset: i64, From 74ac8f798180c053576e5f7b7a24717060e06e16 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Sun, 8 Sep 2024 10:47:50 +0200 Subject: [PATCH 19/19] use null override for custom types --- .../migrations/20240908062042_schema.sql | 2 +- example-postgres/src/main.rs | 17 ++++++++++++++--- ormx-macros/src/table/mod.rs | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/example-postgres/migrations/20240908062042_schema.sql b/example-postgres/migrations/20240908062042_schema.sql index d6e805c..7d92af4 100644 --- a/example-postgres/migrations/20240908062042_schema.sql +++ b/example-postgres/migrations/20240908062042_schema.sql @@ -9,7 +9,7 @@ CREATE TABLE users last_name VARCHAR(128) NOT NULL, email VARCHAR(128) NOT NULL UNIQUE, role user_role NOT NULL, - type account_type NOT NULL, + type account_type, "group" user_group NOT NULL DEFAULT 'local', disabled TEXT, last_login TIMESTAMP DEFAULT NULL diff --git a/example-postgres/src/main.rs b/example-postgres/src/main.rs index 5ab85e8..9dc4d7d 100644 --- a/example-postgres/src/main.rs +++ b/example-postgres/src/main.rs @@ -17,7 +17,7 @@ async fn main() -> anyhow::Result<()> { let db = PgPool::connect(&dotenv::var("DATABASE_URL")?).await?; - log::info!("insert a new row into the database"); + log::info!("insert a few new rows into the database"); let mut new = InsertUser { user_id: 1, first_name: "Moritz".to_owned(), @@ -25,7 +25,18 @@ async fn main() -> anyhow::Result<()> { email: "moritz.bischof1@gmail.com".to_owned(), disabled: None, role: Role::User, - ty: AccountType::Normal, + ty: Some(AccountType::Normal), + } + .insert(&db) + .await?; + InsertUser { + user_id: 2, + first_name: "Dylan".to_owned(), + last_name: "Thomas".to_owned(), + email: "dylan.thomas@gmail.com".to_owned(), + disabled: Some("email not verified".to_owned()), + role: Role::Admin, + ty: None, } .insert(&db) .await?; @@ -82,7 +93,7 @@ struct User { #[ormx(custom_type)] role: Role, #[ormx(column = "type", custom_type)] - ty: AccountType, + ty: Option, #[ormx(custom_type, default, set)] group: UserGroup, disabled: Option, diff --git a/ormx-macros/src/table/mod.rs b/ormx-macros/src/table/mod.rs index 9224322..0dc82bf 100644 --- a/ormx-macros/src/table/mod.rs +++ b/ormx-macros/src/table/mod.rs @@ -72,7 +72,7 @@ impl TableField { if self.custom_type { format!( - "{q}{}{q} AS {q}{}: {}{q}", + "{q}{}{q} AS {q}{}!: {}{q}", self.column_name, self.field.to_string(), self.ty.to_token_stream()