Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 25 additions & 18 deletions sqlx-macros-core/src/query/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!()`
Expand All @@ -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<Expr>,

Expand All @@ -26,26 +26,20 @@ enum QuerySrc {
File(String),
}

pub enum RecordType {
Given(Type),
pub enum OutputType {
GivenRecord(Type),
Scalar,
Generated,
GeneratedRecord(Vec<Meta>),
}

impl Parse for QueryMacroInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut query_src: Option<(QuerySrc, Span)> = None;
let mut args: Option<Vec<Expr>> = 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::<syn::token::Comma>()?;
}

let key: Ident = input.parse()?;

let _ = input.parse::<syn::token::Eq>()?;
Expand All @@ -64,21 +58,30 @@ impl Parse for QueryMacroInput {
let exprs = input.parse::<ExprArray>()?;
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"));
}

// we currently expect only `scalar = _`
// a `query_as_scalar!()` variant seems less useful than just overriding the type
// of the column in SQL
input.parse::<syn::Token![_]>()?;
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::<Meta, Token![,]>::parse_terminated(&content)?
.into_iter()
.collect();
} else if key == "checked" {
let lit_bool = input.parse::<LitBool>()?;
checked = lit_bool.value;
Expand All @@ -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::<Token![,]>()?;
}
}

let (src, src_span) =
Expand All @@ -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,
Expand Down
11 changes: 6 additions & 5 deletions sqlx-macros-core/src/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::<DB>(&data.describe, config, &mut warnings)?;

let record_name: Type = syn::parse_str("Record").unwrap();
Expand All @@ -250,6 +250,7 @@ where
let mut record_tokens = quote! {
#[derive(Debug)]
#[allow(non_snake_case)]
#(#[#attrs])*
struct #record_name {
#(#record_fields)*
}
Expand All @@ -264,12 +265,12 @@ where

record_tokens
}
RecordType::Given(ref out_ty) => {
OutputType::GivenRecord(ref out_ty) => {
let columns = output::columns_to_rust::<DB>(&data.describe, config, &mut warnings)?;

output::quote_query_as::<DB>(&input, out_ty, &query_args, &columns)
}
RecordType::Scalar => output::quote_query_scalar::<DB>(
OutputType::Scalar => output::quote_query_scalar::<DB>(
&input,
config,
&mut warnings,
Expand Down
83 changes: 83 additions & 0 deletions src/macros/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)*)
};
}
Loading