diff --git a/Cargo.lock b/Cargo.lock index 82ea7eaf..53110d27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1308,7 +1308,6 @@ dependencies = [ "dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)", "sqlx-core 0.1.4", "sqlx-macros 0.1.1", ] @@ -1368,7 +1367,6 @@ dependencies = [ "async-std 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "sqlx-core 0.1.4", diff --git a/Cargo.toml b/Cargo.toml index d78f1ffc..855d7b56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ all-features = true [features] default = [ "macros" ] -macros = [ "sqlx-macros", "proc-macro-hack" ] +macros = [ "sqlx-macros" ] tls = ["sqlx-core/tls"] # database @@ -43,7 +43,6 @@ uuid = [ "sqlx-core/uuid", "sqlx-macros/uuid" ] [dependencies] sqlx-core = { version = "=0.1.4", path = "sqlx-core" } sqlx-macros = { version = "0.1.1", path = "sqlx-macros", optional = true } -proc-macro-hack = { version = "0.5.11", optional = true } [dev-dependencies] anyhow = "1.0.26" diff --git a/sqlx-macros/Cargo.toml b/sqlx-macros/Cargo.toml index 622703b3..1ead68f3 100644 --- a/sqlx-macros/Cargo.toml +++ b/sqlx-macros/Cargo.toml @@ -33,7 +33,6 @@ uuid = [ "sqlx/uuid" ] async-std = { version = "1.4.0", default-features = false } dotenv = { version = "0.15.0", default-features = false } futures = { version = "0.3.1", default-features = false } -proc-macro-hack = { version = "0.5.11", default-features = false } proc-macro2 = { version = "1.0.6", default-features = false } sqlx = { version = "0.1.1", default-features = false, path = "../sqlx-core", package = "sqlx-core" } syn = { version = "1.0.11", default-features = false, features = [ "full" ] } diff --git a/sqlx-macros/src/lib.rs b/sqlx-macros/src/lib.rs index ea997c3e..ce1dedf1 100644 --- a/sqlx-macros/src/lib.rs +++ b/sqlx-macros/src/lib.rs @@ -6,8 +6,6 @@ extern crate proc_macro; use proc_macro::TokenStream; -use proc_macro_hack::proc_macro_hack; - use quote::quote; use syn::parse_macro_input; @@ -26,8 +24,22 @@ mod query_macros; use query_macros::*; +fn macro_result(tokens: proc_macro2::TokenStream) -> TokenStream { + quote!( + macro_rules! macro_result { + ($($args:tt)*) => (#tokens) + } + ) + .into() +} + macro_rules! async_macro ( - ($db:ident => $expr:expr) => {{ + ($db:ident, $input:ident: $ty:ty => $expr:expr) => {{ + let $input = match syn::parse::<$ty>($input) { + Ok(input) => input, + Err(e) => return macro_result(e.to_compile_error()), + }; + let res: Result = task::block_on(async { use sqlx::Connection; @@ -70,40 +82,36 @@ macro_rules! async_macro ( Ok(ts) => ts.into(), Err(e) => { if let Some(parse_err) = e.downcast_ref::() { - return dbg!(parse_err).to_compile_error().into(); + macro_result(parse_err.to_compile_error()) + } else { + let msg = format!("{:?}", e); + macro_result(quote!(compile_error(#msg))) } - - let msg = format!("{:?}", e); - quote!(compile_error!(#msg);).into() } } }} ); -#[proc_macro_hack] +#[proc_macro] pub fn query(input: TokenStream) -> TokenStream { #[allow(unused_variables)] - let input = parse_macro_input!(input as QueryMacroInput); - async_macro!(db => expand_query(input, db)) + async_macro!(db, input: QueryMacroInput => expand_query(input, db)) } -#[proc_macro_hack] +#[proc_macro] pub fn query_file(input: TokenStream) -> TokenStream { #[allow(unused_variables)] - let input = parse_macro_input!(input as QueryMacroInput); - async_macro!(db => expand_query_file(input, db)) + async_macro!(db, input: QueryMacroInput => expand_query_file(input, db)) } -#[proc_macro_hack] +#[proc_macro] pub fn query_as(input: TokenStream) -> TokenStream { #[allow(unused_variables)] - let input = parse_macro_input!(input as QueryAsMacroInput); - async_macro!(db => expand_query_as(input, db)) + async_macro!(db, input: QueryAsMacroInput => expand_query_as(input, db)) } -#[proc_macro_hack] +#[proc_macro] pub fn query_file_as(input: TokenStream) -> TokenStream { #[allow(unused_variables)] - let input = parse_macro_input!(input as QueryAsMacroInput); - async_macro!(db => expand_query_file_as(input, db)) + async_macro!(db, input: QueryAsMacroInput => expand_query_file_as(input, db)) } diff --git a/sqlx-macros/src/query_macros/args.rs b/sqlx-macros/src/query_macros/args.rs index f0766c36..7ae7742f 100644 --- a/sqlx-macros/src/query_macros/args.rs +++ b/sqlx-macros/src/query_macros/args.rs @@ -12,7 +12,7 @@ pub fn quote_args( input: &QueryMacroInput, describe: &Describe, ) -> crate::Result { - if input.args.is_empty() { + if input.arg_names.is_empty() { return Ok(quote! { let args = (); }); @@ -22,7 +22,7 @@ pub fn quote_args( let param_types = describe .param_types .iter() - .zip(&*input.args) + .zip(&*input.arg_exprs) .map(|(type_, expr)| { get_type_override(expr) .or_else(|| { @@ -36,7 +36,7 @@ pub fn quote_args( }) .collect::>>()?; - let args_ty_cons = input.args.iter().enumerate().map(|(i, expr)| { + let args_ty_cons = input.arg_names.iter().enumerate().map(|(i, expr)| { // required or `quote!()` emits it as `Nusize` let i = syn::Index::from(i); quote_spanned!( expr.span() => { @@ -56,10 +56,10 @@ pub fn quote_args( TokenStream::new() }; - let args = input.args.iter(); + let args = input.arg_names.iter(); Ok(quote! { - let args = (#(&#args),*,); + let args = (#(&$#args),*,); #args_check }) } diff --git a/sqlx-macros/src/query_macros/input.rs b/sqlx-macros/src/query_macros/input.rs index 3cbae5de..5c6f912e 100644 --- a/sqlx-macros/src/query_macros/input.rs +++ b/sqlx-macros/src/query_macros/input.rs @@ -1,11 +1,15 @@ use std::env; use async_std::fs; -use proc_macro2::Span; +use proc_macro2::{Ident, Span, TokenStream}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::Token; +use syn::spanned::Spanned; +use syn::token::Group; use syn::{Expr, ExprLit, ExprPath, Lit}; +use syn::{ExprGroup, Token}; + +use quote::{format_ident, quote, ToTokens}; use sqlx::describe::Describe; use sqlx::Connection; @@ -14,28 +18,55 @@ use sqlx::Connection; pub struct QueryMacroInput { pub(super) source: String, pub(super) source_span: Span, - pub(super) args: Vec, + // `arg0 .. argN` for N arguments + pub(super) arg_names: Vec, + pub(super) arg_exprs: Vec, } impl QueryMacroInput { fn from_exprs(input: ParseStream, mut args: impl Iterator) -> syn::Result { - let sql = match args.next() { + fn lit_err(span: Span, unexpected: Expr) -> syn::Result { + Err(syn::Error::new( + span, + format!( + "expected string literal, got {}", + unexpected.to_token_stream() + ), + )) + } + + let (source, source_span) = match args.next() { Some(Expr::Lit(ExprLit { lit: Lit::Str(sql), .. - })) => sql, - Some(other_expr) => { - return Err(syn::Error::new_spanned( - other_expr, - "expected string literal", - )); + })) => (sql.value(), sql.span()), + Some(Expr::Group(ExprGroup { + expr, + group_token: Group { span }, + .. + })) => { + // this duplication with the above is necessary because `expr` is `Box` here + // which we can't directly pattern-match without `box_patterns` + match *expr { + Expr::Lit(ExprLit { + lit: Lit::Str(sql), .. + }) => (sql.value(), span), + other_expr => return lit_err(span, other_expr), + } } + Some(other_expr) => return lit_err(other_expr.span(), other_expr), None => return Err(input.error("expected SQL string literal")), }; + let arg_exprs: Vec<_> = args.collect(); + let arg_names = (0..arg_exprs.len()) + .map(|i| format_ident!("arg{}", i)) + .collect(); + Ok(Self { - source: sql.value(), - source_span: sql.span(), - args: args.collect(), + source, + source_span, + arg_exprs, + arg_names, }) } @@ -56,13 +87,13 @@ impl QueryMacroInput { .await .map_err(|e| syn::Error::new(self.source_span, e))?; - if self.args.len() != describe.param_types.len() { + if self.arg_names.len() != describe.param_types.len() { return Err(syn::Error::new( Span::call_site(), format!( "expected {} parameters, got {}", describe.param_types.len(), - self.args.len() + self.arg_names.len() ), ) .into()); @@ -97,16 +128,33 @@ impl QueryAsMacroInput { impl Parse for QueryAsMacroInput { fn parse(input: ParseStream) -> syn::Result { + fn path_err(span: Span, unexpected: Expr) -> syn::Result { + Err(syn::Error::new( + span, + format!( + "expected path to a type, got {}", + unexpected.to_token_stream() + ), + )) + } + let mut args = Punctuated::::parse_terminated(input)?.into_iter(); let as_ty = match args.next() { Some(Expr::Path(path)) => path, - Some(other_expr) => { - return Err(syn::Error::new_spanned( - other_expr, - "expected path to a type", - )); + Some(Expr::Group(ExprGroup { + expr, + group_token: Group { span }, + .. + })) => { + // this duplication with the above is necessary because `expr` is `Box` here + // which we can't directly pattern-match without `box_patterns` + match *expr { + Expr::Path(path) => path, + other_expr => return path_err(span, other_expr), + } } + Some(other_expr) => return path_err(other_expr.span(), other_expr), None => return Err(input.error("expected path to SQL file")), }; diff --git a/sqlx-macros/src/query_macros/mod.rs b/sqlx-macros/src/query_macros/mod.rs index 144d4285..e6013cb8 100644 --- a/sqlx-macros/src/query_macros/mod.rs +++ b/sqlx-macros/src/query_macros/mod.rs @@ -46,6 +46,7 @@ where } let args_tokens = args::quote_args(&input.query_input, &describe)?; + let arg_names = &input.query_input.arg_names; let columns = output::columns_to_rust(&describe)?; let output = output::quote_query_as::( @@ -54,10 +55,14 @@ where &columns, ); - Ok(quote! {{ - #args_tokens - #output.bind_all(args) - }}) + Ok(quote! { + macro_rules! macro_result { + (#($#arg_names:expr),*) => {{ + #args_tokens + #output.bind_all(args) + }} + } + }) } pub async fn expand_query_file_as( diff --git a/sqlx-macros/src/query_macros/query.rs b/sqlx-macros/src/query_macros/query.rs index 5b50b26c..bf493819 100644 --- a/sqlx-macros/src/query_macros/query.rs +++ b/sqlx-macros/src/query_macros/query.rs @@ -50,15 +50,20 @@ where .collect::(); let output = output::quote_query_as::(sql, &record_type, &columns); + let arg_names = &input.arg_names; - Ok(quote! {{ - #[derive(Debug)] - struct #record_type { - #record_fields - } + Ok(quote! { + macro_rules! macro_result { + (#($#arg_names:expr),*) => {{ + #[derive(Debug)] + struct #record_type { + #record_fields + } - #args + #args - #output.bind_all(args) - }}) + #output.bind_all(args) + } + }} + }) } diff --git a/src/lib.rs b/src/lib.rs index 17a9c085..2df1ea04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,34 +20,14 @@ pub use sqlx_core::mysql::{self, MySql, MySqlConnection, MySqlPool}; #[cfg(feature = "postgres")] pub use sqlx_core::postgres::{self, PgConnection, PgPool, Postgres}; -#[allow(unused_attributes)] +#[cfg(feature = "macros")] +#[doc(hidden)] +pub extern crate sqlx_macros; + +#[cfg(feature = "macros")] #[macro_export] mod macros; -#[cfg(feature = "macros")] -#[doc(hidden)] -#[proc_macro_hack::proc_macro_hack(fake_call_site)] -#[allow(dead_code)] -pub use sqlx_macros::query as query_; - -#[cfg(feature = "macros")] -#[doc(hidden)] -#[proc_macro_hack::proc_macro_hack(fake_call_site)] -#[allow(dead_code)] -pub use sqlx_macros::query_as as query_as_; - -#[cfg(feature = "macros")] -#[doc(hidden)] -#[proc_macro_hack::proc_macro_hack(fake_call_site)] -#[allow(dead_code)] -pub use sqlx_macros::query_file as query_file_; - -#[cfg(feature = "macros")] -#[doc(hidden)] -#[proc_macro_hack::proc_macro_hack(fake_call_site)] -#[allow(dead_code)] -pub use sqlx_macros::query_file_as as query_file_as_; - // macro support #[cfg(feature = "macros")] #[doc(hidden)] diff --git a/src/macros.rs b/src/macros.rs index afbef614..9086bffd 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -82,18 +82,23 @@ /// * [query_as!] if you want to use a struct you can name, /// * [query_file!] if you want to define the SQL query out-of-line, /// * [query_file_as!] if you want both of the above. -#[cfg(feature = "macros")] #[macro_export] macro_rules! query ( - // the emitted item for `#[proc_macro_hack]` doesn't look great in docs - // plus this might let IDEs hint at the syntax - // `#[allow(dead_code)]` to silence the `enum ProcMacroHack` error - ($query:literal) => (#[allow(dead_code)] { - $crate::query_!($query) + // by emitting a macro definition from our proc-macro containing the result tokens, + // we no longer have a need for `proc-macro-hack` + ($query:literal) => ({ + #[macro_use] + mod _macro_result { + $crate::sqlx_macros::query!($query); + } + macro_result!() }); - ($query:literal, $($args:tt)*) => (#[allow(dead_code)]{ - #![allow(dead_code)] - $crate::query_!($query, $($args)*) + ($query:literal, $($args:expr),*) => ({ + #[macro_use] + mod _macro_result { + $crate::sqlx_macros::query!($query, $($args),*); + } + macro_result!($($args),*) }) ); @@ -139,14 +144,21 @@ macro_rules! query ( /// # #[cfg(not(feature = "mysql"))] /// # fn main() {} /// ``` -#[cfg(feature = "macros")] #[macro_export] macro_rules! query_file ( ($query:literal) => (#[allow(dead_code)]{ - $crate::query_file_!($query) + #[macro_use] + mod _macro_result { + $crate::sqlx_macros::query_file!($query); + } + macro_result!() }); - ($query:literal, $($args:tt)*) => (#[allow(dead_code)]{ - $crate::query_file_!($query, $($args)*) + ($query:literal, $($args:expr),*) => (#[allow(dead_code)]{ + #[macro_use] + mod _macro_result { + $crate::sqlx_macros::query_file!($query, $($args),*); + } + macro_result!($($args),*) }) ); @@ -197,14 +209,21 @@ macro_rules! query_file ( /// # #[cfg(not(feature = "mysql"))] /// # fn main() {} /// ``` -#[cfg(feature = "macros")] #[macro_export] macro_rules! query_as ( ($out_struct:path, $query:literal) => (#[allow(dead_code)] { - $crate::query_as_!($out_struct, $query) + #[macro_use] + mod _macro_result { + $crate::sqlx_macros::query_as!($out_struct, $query); + } + macro_result!() }); - ($out_struct:path, $query:literal, $($args:tt)*) => (#[allow(dead_code)] { - $crate::query_as_!($out_struct, $query, $($args)*) + ($out_struct:path, $query:literal, $($args:expr),*) => (#[allow(dead_code)] { + #[macro_use] + mod _macro_result { + $crate::sqlx_macros::query_as!($out_struct, $query, $($args),*); + } + macro_result!($($args),*) }) ); @@ -240,13 +259,20 @@ macro_rules! query_as ( /// # #[cfg(not(feature = "mysql"))] /// # fn main() {} /// ``` -#[cfg(feature = "macros")] #[macro_export] macro_rules! query_file_as ( ($out_struct:path, $query:literal) => (#[allow(dead_code)] { - $crate::query_file_as_!($out_struct, $query) + #[macro_use] + mod _macro_result { + $crate::sqlx_macros::query_file_as!($out_struct, $query); + } + macro_result!() }); - ($out_struct:path, $query:literal, $($args:tt)*) => (#[allow(dead_code)] { - $crate::query_file_as_!($out_struct, $query, $($args)*) + ($out_struct:path, $query:literal, $($args:expr),*) => (#[allow(dead_code)] { + #[macro_use] + mod _macro_result { + $crate::sqlx_macros::query_file_as!($out_struct, $query, $($args),*); + } + macro_result!($($args),*) }) ); diff --git a/tests/mysql.rs b/tests/mysql.rs index b13741bd..0085b2eb 100644 --- a/tests/mysql.rs +++ b/tests/mysql.rs @@ -73,6 +73,21 @@ async fn pool_immediately_fails_with_db_error() -> anyhow::Result<()> { #[cfg(feature = "macros")] #[async_std::test] async fn macro_select_from_cte() -> anyhow::Result<()> { + let mut conn = connect().await?; + let account = + sqlx::query!("select * from (select (1) as id, 'Herp Derpinson' as name) accounts") + .fetch_one(&mut conn) + .await?; + + println!("{:?}", account); + println!("{}: {}", account.id, account.name); + + Ok(()) +} + +#[cfg(feature = "macros")] +#[async_std::test] +async fn macro_select_from_cte_bind() -> anyhow::Result<()> { let mut conn = connect().await?; let account = sqlx::query!( "select * from (select (1) as id, 'Herp Derpinson' as name) accounts where id = ?",