Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support conditional queries in query_as! #1491

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c7bbcd1
WIP: initial implementation of conditional_query_as! (#1488)
NyxCode Oct 13, 2021
7844fc4
restructure code
NyxCode Oct 25, 2021
0d0028c
make conditional_query_as! expand to just sqlx::query! if no branches…
NyxCode Oct 26, 2021
8b7fb08
support inline arguments and remove `?<arg>` syntax
NyxCode Oct 26, 2021
3301a60
Merge branch 'master' into conditional-query
NyxCode Oct 26, 2021
4f2ce57
allow concatenation of segments for backwards compatibility
NyxCode Oct 26, 2021
21060ce
make use of format_ident!
NyxCode Oct 26, 2021
dabef59
make calls to expand_query! internally, make (hopefully) completely b…
NyxCode Oct 27, 2021
1f10f64
fix bad call to quote!
NyxCode Oct 27, 2021
860b72d
accidentally swapped { and }
NyxCode Oct 27, 2021
89878ed
add test, clean up
NyxCode Oct 27, 2021
39ae6c2
don't hide query_as! from docs
NyxCode Oct 27, 2021
0601ddb
Merge branch 'master' into conditional-query
NyxCode Mar 2, 2022
6489cdc
add brief summary to conditional/mod.rs
NyxCode Mar 2, 2022
b483b98
enable CI for development
NyxCode Mar 2, 2022
1a72621
add documentation, format, derive Debug for QuerySegment and Context
NyxCode Mar 2, 2022
7ff06a5
fix parsing of trailing comma
NyxCode Mar 2, 2022
5767150
re-export futures_core from sqlx-core
NyxCode Mar 9, 2022
aade05a
fix return type of conditional map fetch method
NyxCode Mar 9, 2022
f280e67
add simple test
NyxCode Mar 9, 2022
64b0ff6
re-export futures_core from sqlx
NyxCode Mar 9, 2022
1fc3697
add an other test
NyxCode Mar 9, 2022
fb072bb
remove unused imports
NyxCode Mar 9, 2022
ac4e02d
fix tests
NyxCode Mar 9, 2022
024c9be
tests: fix nullability issue
NyxCode Mar 9, 2022
d45d86b
tests: add dynamic_filtering test
NyxCode Mar 9, 2022
046c2dd
docs: add user-facing documentation
NyxCode Mar 9, 2022
11be4f3
tests: test inline arguments in query_as!
NyxCode Mar 9, 2022
bbe8d07
add debug log
NyxCode Mar 9, 2022
5a29c81
fix off-by-one in parameter generation
NyxCode Mar 9, 2022
85065a1
fix unused import
NyxCode Mar 9, 2022
bae5dd6
re-run CI
NyxCode Mar 9, 2022
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
2 changes: 0 additions & 2 deletions .github/workflows/sqlx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ name: SQLx
on:
pull_request:
push:
branches:
- master
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove this?


jobs:
format:
Expand Down
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ name = "postgres-derives"
path = "tests/postgres/derives.rs"
required-features = ["postgres", "macros"]

[[test]]
name = "postgres-conditonal_query"
path = "tests/postgres/conditional_query.rs"
required-features = ["postgres", "macros"]

#
# Microsoft SQL Server (MSSQL)
#
Expand Down
3 changes: 3 additions & 0 deletions sqlx-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,6 @@ pub mod mssql;
/// sqlx uses ahash for increased performance, at the cost of reduced DoS resistance.
use ahash::AHashMap as HashMap;
//type HashMap<K, V> = std::collections::HashMap<K, V, ahash::RandomState>;

#[doc(hidden)]
pub use futures_core;
2 changes: 1 addition & 1 deletion sqlx-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,6 @@ sqlx-rt = { version = "0.5.11", default-features = false, path = "../sqlx-rt" }
serde = { version = "1.0.132", features = ["derive"], optional = true }
serde_json = { version = "1.0.73", optional = true }
sha2 = { version = "0.9.8", optional = true }
syn = { version = "1.0.84", default-features = false, features = ["full"] }
syn = { version = "1.0.84", default-features = false, features = ["full", "extra-traits"] }
quote = { version = "1.0.14", default-features = false }
url = { version = "2.2.2", default-features = false }
204 changes: 204 additions & 0 deletions sqlx-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,210 @@ pub fn expand_query(input: TokenStream) -> TokenStream {
}
}

/// A variant of [query!] which takes a path to an explicitly defined struct as the output type.
///
/// This lets you return the struct from a function or add your own trait implementations.
///
/// **No trait implementations are required**; the macro maps rows using a struct literal
/// where the names of columns in the query are expected to be the same as the fields of the struct
/// (but the order does not need to be the same). The types of the columns are based on the
/// query and not the corresponding fields of the struct, so this is type-safe as well.
///
/// This enforces a few things:
/// * The query must output at least one column.
/// * The column names of the query must match the field names of the struct.
/// * The field types must be the Rust equivalent of their SQL counterparts; see the corresponding
/// module for your database for mappings:
/// * Postgres: [crate::postgres::types]
/// * MySQL: [crate::mysql::types]
/// * SQLite: [crate::sqlite::types]
/// * MSSQL: [crate::mssql::types]
/// * If a column may be `NULL`, the corresponding field's type must be wrapped in `Option<_>`.
/// * Neither the query nor the struct may have unused fields.
///
/// In contrast to the syntax of `query!()`, the struct name is given before the SQL
/// string. Arguments may be passed, like in `query!()`, within a comma-seperated list after the
/// SQL string, or inline within the query string using `"{<EXPRESSION>}"`:
/// ```rust,ignore
/// # use sqlx::Connect;
/// # #[cfg(all(feature = "mysql", feature = "_rt-async-std"))]
/// # #[async_std::main]
/// # async fn main() -> sqlx::Result<()>{
/// # let db_url = dotenv::var("DATABASE_URL").expect("DATABASE_URL must be set");
/// #
/// # if !(db_url.starts_with("mysql") || db_url.starts_with("mariadb")) { return Ok(()) }
/// # let mut conn = sqlx::MySqlConnection::connect(db_url).await?;
/// #[derive(Debug)]
/// struct Account {
/// id: i32,
/// name: String
/// }
///
/// // let mut conn = <impl sqlx::Executor>;
/// let id = 1;
/// let account = sqlx::query_as!(
/// Account,
/// "select * from (select (1) as id, 'Herp Derpinson' as name) accounts where id = {id}"
/// )
/// .fetch_one(&mut conn)
/// .await?;
///
/// println!("{:?}", account);
/// println!("{}: {}", account.id, account.name);
///
/// # Ok(())
/// # }
/// #
/// # #[cfg(any(not(feature = "mysql"), not(feature = "_rt-async-std")))]
/// # fn main() {}
/// ```
///
/// **The method you want to call depends on how many rows you're expecting.**
///
/// | Number of Rows | Method to Call* | Returns (`T` being the given struct) | Notes |
/// |----------------| ----------------------------|----------------------------------------|-------|
/// | Zero or One | `.fetch_optional(...).await`| `sqlx::Result<Option<T>>` | Extra rows are ignored. |
/// | Exactly One | `.fetch_one(...).await` | `sqlx::Result<T>` | Errors if no rows were returned. Extra rows are ignored. Aggregate queries, use this. |
/// | At Least One | `.fetch(...)` | `impl Stream<Item = sqlx::Result<T>>` | Call `.try_next().await` to get each row result. |
/// | Multiple | `.fetch_all(...)` | `sqlx::Result<Vec<T>>` | |
///
/// \* All methods accept one of `&mut {connection type}`, `&mut Transaction` or `&Pool`.
/// (`.execute()` is omitted as this macro requires at least one column to be returned.)
///
/// ### Column Type Override: Infer from Struct Field
/// In addition to the column type overrides supported by [query!], `query_as!()` supports an
/// additional override option:
///
/// If you select a column `foo as "foo: _"` (Postgres/SQLite) or `` foo as `foo: _` `` (MySQL)
/// it causes that column to be inferred based on the type of the corresponding field in the given
/// record struct. Runtime type-checking is still done so an error will be emitted if the types
/// are not compatible.
///
/// This allows you to override the inferred type of a column to instead use a custom-defined type:
///
/// ```rust,ignore
/// #[derive(sqlx::Type)]
/// #[sqlx(transparent)]
/// struct MyInt4(i32);
///
/// struct Record {
/// id: MyInt4,
/// }
///
/// let my_int = MyInt4(1);
///
/// // Postgres/SQLite
/// sqlx::query_as!(Record, r#"select 1 as "id: _""#) // MySQL: use "select 1 as `id: _`" instead
/// .fetch_one(&mut conn)
/// .await?;
///
/// assert_eq!(record.id, MyInt4(1));
/// ```
///
/// ### Conditional Queries
/// This macro allows you to dynamically construct queries at runtime, while still ensuring that
/// they are checked at compile-time.
///
/// Let's consider an example first. Let's say you want to query all products from your database,
/// while the user may decide if he wants them ordered in ascending or descending order.
/// This could be achieved by writing both queries out by hand:
/// ```rust,ignore
/// let products = if order_ascending {
/// sqlx::query_as!(
/// Product,
/// "SELECT * FROM products ORDER BY name ASC"
/// )
/// .fetch_all(&mut con)
/// .await?
/// } else {
/// sqlx::query_as!(
/// Product,
/// "SELECT * FROM products ORDER BY name DESC"
/// )
/// .fetch_all(&mut con)
/// .await?
/// };
/// ```
/// To avoid repetition in these cases, you may use `if`, `if let` and `match` directly within the macro
/// invocation:
/// ```rust,ignore
/// let products = sqlx::query_as!(
/// Product,
/// "SELECT * FROM products ORDER BY NAME"
/// if order_ascending { "ASC" } else { "DESC" }
/// )
/// .fetch_all(&mut con)
/// .await?;
/// ```
/// The macro will expand to something similar like in the verbose example above, ensuring that
/// every possible query which may result from the macro invocation is checked at compile-time.
///
/// When writing *conditional* queries, parameters may only be given inline.
///
/// It is recommended to avoid using a lot of `if` and `match` clauses within a single `query_as!`
/// invocation. Do not use much more than 6 within a single query to avoid drastically increased
/// compile times.
///
/// ### Troubleshooting: "error: mismatched types"
/// If you get a "mismatched types" error from an invocation of this macro and the error
/// isn't pointing specifically at a parameter.
///
/// For example, code like this (using a Postgres database):
///
/// ```rust,ignore
/// struct Account {
/// id: i32,
/// name: Option<String>,
/// }
///
/// let account = sqlx::query_as!(
/// Account,
/// r#"SELECT id, name from (VALUES (1, 'Herp Derpinson')) accounts(id, name)"#,
/// )
/// .fetch_one(&mut conn)
/// .await?;
/// ```
///
/// Might produce an error like this:
/// ```text,ignore
/// error[E0308]: mismatched types
/// --> tests/postgres/macros.rs:126:19
/// |
/// 126 | let account = sqlx::query_as!(
/// | ___________________^
/// 127 | | Account,
/// 128 | | r#"SELECT id, name from (VALUES (1, 'Herp Derpinson')) accounts(id, name)"#,
/// 129 | | )
/// | |_____^ expected `i32`, found enum `std::option::Option`
/// |
/// = note: expected type `i32`
/// found enum `std::option::Option<i32>`
/// ```
///
/// This means that you need to check that any field of the "expected" type (here, `i32`) matches
/// the Rust type mapping for its corresponding SQL column (see the `types` module of your database,
/// listed above, for mappings). The "found" type is the SQL->Rust mapping that the macro chose.
///
/// In the above example, the returned column is inferred to be nullable because it's being
/// returned from a `VALUES` statement in Postgres, so the macro inferred the field to be nullable
/// and so used `Option<i32>` instead of `i32`. **In this specific case** we could use
/// `select id as "id!"` to override the inferred nullability because we know in practice
/// that column will never be `NULL` and it will fix the error.
///
/// Nullability inference and type overrides are discussed in detail in the docs for [query!].
///
/// It unfortunately doesn't appear to be possible right now to make the error specifically mention
/// the field; this probably requires the `const-panic` feature (still unstable as of Rust 1.45).
#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
#[proc_macro]
pub fn query_as(input: TokenStream) -> TokenStream {
match query::query_as(input.into()) {
Ok(output) => output,
Err(err) => err.to_compile_error(),
}
.into()
}

#[proc_macro_derive(Encode, attributes(sqlx))]
pub fn derive_encode(tokenstream: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(tokenstream as syn::DeriveInput);
Expand Down
80 changes: 80 additions & 0 deletions sqlx-macros/src/query/conditional/map.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote};

pub fn generate_conditional_map(n: usize) -> TokenStream {
let map_fns = (1..=n).map(|i| format_ident!("F{}", i)).collect::<Vec<_>>();
let args = (1..=n).map(|i| format_ident!("A{}", i)).collect::<Vec<_>>();
let variants = (1..=n).map(|i| format_ident!("_{}", i)).collect::<Vec<_>>();
let variant_declarations = (0..n).map(|i| {
let variant = &variants[i];
let map_fn = &map_fns[i];
let args = &args[i];
quote!(#variant(sqlx::query::Map<'q, DB, #map_fn, #args>))
});

quote! {
#[doc(hidden)]
pub enum ConditionalMap<'q, DB, O, #(#map_fns,)* #(#args,)*>
where
DB: sqlx::Database,
O: Send + Unpin,
#(#map_fns: FnMut(DB::Row) -> sqlx::Result<O> + Send,)*
#(#args: 'q + Send + sqlx::IntoArguments<'q, DB>,)*
{
#(#variant_declarations),*
}
impl<'q, DB, O, #(#map_fns,)* #(#args,)*> ConditionalMap<'q, DB, O, #(#map_fns,)* #(#args,)*>
where
DB: sqlx::Database,
O: Send + Unpin,
#(#map_fns: FnMut(DB::Row) -> sqlx::Result<O> + Send,)*
#(#args: 'q + Send + sqlx::IntoArguments<'q, DB>,)*
{
pub fn fetch<'e, 'c: 'e, E>(self, executor: E) -> sqlx::futures_core::stream::BoxStream<'e, sqlx::Result<O>>
where
'q: 'e,
E: 'e + sqlx::Executor<'c, Database = DB>,
DB: 'e,
O: 'e,
#(#map_fns: 'e,)*
{
match self { #(
Self::#variants(x) => x.fetch(executor)
),* }
}
pub async fn fetch_all<'e, 'c: 'e, E>(self, executor: E) -> sqlx::Result<Vec<O>>
where
'q: 'e,
DB: 'e,
E: 'e + sqlx::Executor<'c, Database = DB>,
O: 'e
{
match self { #(
Self::#variants(x) => x.fetch_all(executor).await
),* }
}
pub async fn fetch_one<'e, 'c: 'e, E>(self, executor: E) -> sqlx::Result<O>
where
'q: 'e,
E: 'e + sqlx::Executor<'c, Database = DB>,
DB: 'e,
O: 'e,
{
match self { #(
Self::#variants(x) => x.fetch_one(executor).await
),* }
}
pub async fn fetch_optional<'e, 'c: 'e, E>(self, executor: E) -> sqlx::Result<Option<O>>
where
'q: 'e,
E: 'e + sqlx::Executor<'c, Database = DB>,
DB: 'e,
O: 'e,
{
match self { #(
Self::#variants(x) => x.fetch_optional(executor).await
),* }
}
}
}
}
Loading