From dc3eaf3e94a2ef6309d6a0a18a24794fcfb5dbaf Mon Sep 17 00:00:00 2001 From: jacobs Date: Mon, 24 Nov 2025 17:34:56 +0000 Subject: [PATCH] Added queryx with support for attributes --- sqlx-macros-core/src/query/input.rs | 43 ++++++++------- sqlx-macros-core/src/query/mod.rs | 11 ++-- src/macros/mod.rs | 83 +++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 23 deletions(-) diff --git a/sqlx-macros-core/src/query/input.rs b/sqlx-macros-core/src/query/input.rs index 63e35ec77d..350af29094 100644 --- a/sqlx-macros-core/src/query/input.rs +++ b/sqlx-macros-core/src/query/input.rs @@ -3,7 +3,7 @@ use std::fs; use proc_macro2::{Ident, Span}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::{Expr, LitBool, LitStr, Token}; +use syn::{bracketed, Expr, LitBool, LitStr, Meta, Token}; use syn::{ExprArray, Type}; /// Macro input shared by `query!()` and `query_file!()` @@ -12,7 +12,7 @@ pub struct QueryMacroInput { pub(super) src_span: Span, - pub(super) record_type: RecordType, + pub(super) output_type: OutputType, pub(super) arg_exprs: Vec, @@ -26,26 +26,20 @@ enum QuerySrc { File(String), } -pub enum RecordType { - Given(Type), +pub enum OutputType { + GivenRecord(Type), Scalar, - Generated, + GeneratedRecord(Vec), } impl Parse for QueryMacroInput { fn parse(input: ParseStream) -> syn::Result { let mut query_src: Option<(QuerySrc, Span)> = None; let mut args: Option> = None; - let mut record_type = RecordType::Generated; + let mut output_type = OutputType::GeneratedRecord(Vec::new()); let mut checked = true; - let mut expect_comma = false; - while !input.is_empty() { - if expect_comma { - let _ = input.parse::()?; - } - let key: Ident = input.parse()?; let _ = input.parse::()?; @@ -64,13 +58,13 @@ impl Parse for QueryMacroInput { let exprs = input.parse::()?; args = Some(exprs.elems.into_iter().collect()) } else if key == "record" { - if !matches!(record_type, RecordType::Generated) { + if !matches!(output_type, OutputType::GeneratedRecord(_)) { return Err(input.error("colliding `scalar` or `record` key")); } - record_type = RecordType::Given(input.parse()?); + output_type = OutputType::GivenRecord(input.parse()?); } else if key == "scalar" { - if !matches!(record_type, RecordType::Generated) { + if !matches!(output_type, OutputType::GeneratedRecord(_)) { return Err(input.error("colliding `scalar` or `record` key")); } @@ -78,7 +72,16 @@ impl Parse for QueryMacroInput { // a `query_as_scalar!()` variant seems less useful than just overriding the type // of the column in SQL input.parse::()?; - record_type = RecordType::Scalar; + output_type = OutputType::Scalar; + } else if key == "attrs" { + let OutputType::GeneratedRecord(ref mut attrs) = output_type else { + return Err(input.error("can only set attributes for generated type")); + }; + let content; + bracketed!(content in input); + *attrs = Punctuated::::parse_terminated(&content)? + .into_iter() + .collect(); } else if key == "checked" { let lit_bool = input.parse::()?; checked = lit_bool.value; @@ -87,7 +90,11 @@ impl Parse for QueryMacroInput { return Err(syn::Error::new_spanned(key, message)); } - expect_comma = true; + if input.is_empty() { + break; + } else { + input.parse::()?; + } } let (src, src_span) = @@ -100,7 +107,7 @@ impl Parse for QueryMacroInput { Ok(QueryMacroInput { sql: src.resolve(src_span)?, src_span, - record_type, + output_type, arg_exprs, checked, file_path, diff --git a/sqlx-macros-core/src/query/mod.rs b/sqlx-macros-core/src/query/mod.rs index 84461f22b8..9fed1d9197 100644 --- a/sqlx-macros-core/src/query/mod.rs +++ b/sqlx-macros-core/src/query/mod.rs @@ -10,7 +10,7 @@ use sqlx_core::{column::Column, describe::Describe, type_info::TypeInfo}; use crate::database::DatabaseExt; use crate::query::data::{hash_string, DynQueryData, QueryData}; -use crate::query::input::RecordType; +use crate::query::input::OutputType; use crate::query::metadata::MacrosEnv; use either::Either; use metadata::Metadata; @@ -227,8 +227,8 @@ where ::sqlx::__query_with_result::<#db_path, _>(#sql, #query_args) } } else { - match input.record_type { - RecordType::Generated => { + match input.output_type { + OutputType::GeneratedRecord(ref attrs) => { let columns = output::columns_to_rust::(&data.describe, config, &mut warnings)?; let record_name: Type = syn::parse_str("Record").unwrap(); @@ -250,6 +250,7 @@ where let mut record_tokens = quote! { #[derive(Debug)] #[allow(non_snake_case)] + #(#[#attrs])* struct #record_name { #(#record_fields)* } @@ -264,12 +265,12 @@ where record_tokens } - RecordType::Given(ref out_ty) => { + OutputType::GivenRecord(ref out_ty) => { let columns = output::columns_to_rust::(&data.describe, config, &mut warnings)?; output::quote_query_as::(&input, out_ty, &query_args, &columns) } - RecordType::Scalar => output::quote_query_scalar::( + OutputType::Scalar => output::quote_query_scalar::( &input, config, &mut warnings, diff --git a/src/macros/mod.rs b/src/macros/mod.rs index 0db6f0c2e7..9765acc477 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -869,3 +869,86 @@ macro_rules! migrate { $crate::sqlx_macros::migrate!() }}; } + +/// A variant of [`query!`][`crate::query!`] that combines the functionality of the other query +/// macros. +/// - [`query_unchecked!`][`crate::query_unchecked!] +/// - [`query_file!`][`crate::query_file!] +/// - [`query_file_unchecked!`][`crate::query_file_unchecked!] +/// - [`query_as!`][`crate::query_as!] +/// - [`query_file_as!`][`crate::query_file_as!] +/// - [`query_as_unchecked!`][`crate::query_as_unchecked!] +/// - [`query_file_as_unchecked!`][`crate::query_file_as_unchecked!] +/// - [`query_scalar!`][`crate::query_scalar!] +/// - [`query_file_scalar!`][`crate::query_file_scalar!] +/// - [`query_scalar_unchecked!`][`crate::query_scalar_unchecked!] +/// - [`query_file_scalar_unchecked!`][`crate::query_file_scalar_unchecked!] +/// +/// The syntax is +/// - Flags (Optional) +/// - Query +/// - Parameters (Optional) +/// - Type (Optional) +/// +/// # Flags +/// Zero or more flags. Currently the only supported flag is `unchecked`, which changes the +/// checking behavior to be like [`query_unchecked!`][`crate::query_unchecked!`] +/// +/// # Query +/// A string literal, or `file("path")` to read the query from a file. +/// +/// The query itself is the same as in [`query!`][`crate::query!`] with regards to type and +/// nullability overrides. +/// +/// This macro does not support joining multiple literals into a single query with `+`. +/// +/// # Parameters +/// A comma separated list of expressions wrapped in parenthesis. +/// +/// The syntax is intended to look like a function call. +/// +/// # Type +/// Information about the desired return type can be given after a `:`, +/// if this section is omitted the type will be inferred, and will derive `Debug` only. +/// +/// There are multiple options for what can come after the `:` +/// - An identifier: behaves like [`query_as!`][`crate::query_as!`] +/// - `scalar`: behaves like [`query_scalar!`][`crate::query_scalar!`] +/// - Zero or more attributes: infers the return type, and applies the provided attributes in +/// addition to deriving `Debug` +#[macro_export] +macro_rules! queryx { + (@query ($($acc:tt)*) unchecked $($rest:tt)*) => { + $crate::queryx!(@query (checked = false, $($acc)*) $($rest)*) + }; + (@query ($($acc:tt)*) $query:literal $($rest:tt)*) => { + $crate::queryx!(@args (source = $query, $($acc)*) $($rest)*) + }; + (@query ($($acc:tt)*) file($path:literal) $($rest:tt)*) => { + $crate::queryx!(@args (source_file = $path, $($acc)*) $($rest)*) + }; + (@args ($($acc:tt)*) $(($($args:expr),* $(,)?))? $(: $($rest:tt)*)?) => { + $crate::queryx!(@type ($(args = [$($args),*],)? $($acc)*) $($($rest)*)?) + }; + + (@type ($($acc:tt)*) ) => { + $crate::sqlx_macros::expand_query!($($acc)*) + }; + (@type ($($acc:tt)*) $given_type:ty ) => { + $crate::sqlx_macros::expand_query!(record = $given_type, $($acc)*) + }; + (@type ($($acc:tt)*) FromRow $type:ty ) => { + $crate::sqlx_macros::expand_query!(from_row = $type, $($acc)*) + }; + (@type ($($acc:tt)*) $(#[$attrs:meta])* ) => { + $crate::sqlx_macros::expand_query!(attrs = [$($attrs),*], $($acc)*) + }; + (@type ($($acc:tt)*) scalar ) => { + $crate::sqlx_macros::expand_query!(scalar = _, $($acc)*) + }; + + // Entrypoint + ($($rest:tt)*) => { + $crate::queryx!(@query () $($rest)*) + }; +}