-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add macro for checking dynamic queries at compile time #1488
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
Comments
This is something that's been hotly requested for a long time. I'm interested, but I have a couple of comments:
sqlx::query_as!(
User,
r#"
SELECT *
FROM users
WHERE
email = {query}
OR first_name LIKE {query}
OR last_name LIKE {query}
ORDER BY
{
match order_by {
OrderBy::Name => "first_name",
OrderBy::UserId => "id",
}
}
{
if let Some(limit) = limit {
"LIMIT {limit}"
}
}
"#
) I think this way, we at least have a chance of retaining some syntax highlighting in IDEs like CLion as we can make |
How about something like sqlx::query_as!(
User,
r#"
SELECT *
FROM users
WHERE
email = {query}
OR first_name LIKE {query}
OR last_name LIKE {query}
ORDER BY {#order_col} {#limit}
"#,
#order_col = match order_by {
OrderBy::Name => "first_name",
OrderBy::UserId => "id",
}
#limit = match limit {
Some(limit) => "LIMIT {limit}",
None => "",
}
) using a special sigil like |
I like this a lot! Would also give nice syntax highlighting since not everything is in a big string literal |
@jplatte Though I'm not sure how nested branches would look like, maybe something like #order_col = if let Some((column, direction)) = order {
"ORDER BY {#column} {#direction}",
#direction = match direction {
Order::Ascending => "ASC",
Order::Descending => "DESC"
},
#column = match column {
Column::Id => "id",
...
}
} ? |
EDIT: Initial idea doesn't work. Let me think more about it... |
I'm now at the point where I'm trying to figure out how to integrate this into My approach to this would just be to keep the old macro under a new name, and in calls to This is obviously more like a hack than a proper solution. That being said, I'm not familiar with the rest of the sqlx codebase and not sure how to properly integrate it. |
EDIT: I have now made conditional_query_as! expand to |
#1491 now passes all tests, so it's backwards-compatible as far as the tests can tell. inline argumentsThis currently supports the "basic" inline arguments of #875: Everything within |
TODOs:
|
Sorry for bumping, but are there any updates on this? |
@TmLev it's been commented on by @abonander, suggesting this won't happen: #1491 (comment) |
Hey! As this is no longer planned to be included in SQLx upstream we decided to implement it as a separate crate since we have a need for it. sqlx-conditional-queries (github). |
I fully support this proposal! When I use string constants or macros to extract the first half of the query string, the compiler tells me that query_as! is not supported, but if I use query_as, it cannot check SQL statement errors at compile time. No way, I would rather copy the query statement than not have the compile-time check function. TOO LONG my code:
|
The argument against this seems to be the combinatorial explosion from checking all known values, which is reasonable. As an alternative, what about partially checked queries? That is, validate a portion of the query at compile time to get type information, but allow editing the query to fill in missing information. Something like the following could be a possible API: struct User { id: u32, name: String, birthday: NaiveDateTime }
/// Return users with a name matcher, born after a specific date,
/// ordering by any valid column
fn sort_filter(
pool: &MySqlPool,
name_like: &str,
min_birthday: Option<NaiveDateTime>,
order_col: Ordering<&str>
) -> sqlx::Result<Vec<Table>> {
// Initialize the checked portion of the query
let mut pq: PartialQuery = sqlx::query_as!(
User,
"SELECT id, name, birthday
FROM t1
WHERE name LIKE ?
dynamic!("where_addition")
dynamic!("order_by")
LIMIT 100",
name_like
);
// `q` is a reference to the query right before `where_clause`
pq.set("where_addition", |q| {
if let Some(bday) = min_birthday {
q.push("AND birthday > ").push_bind(bday);
}
})?;
// Make sure our query will be correct. Bonus if `PartialQuery` could pull
// a static list of all table columns at compile time, to make this even easier.
check_column_valid!(User, order_col)?;
// Set the other dynamic placeholder
pq.set("order_by", |q| {
q.push("ORDER_BY");
match order_col {
Ordering::Asc(v) => q.push('`').push(v).push("` ").push("ASC");
Ordering::Desc(v) => q.push('`').push(v).push("` ").push("DESC");
}
})?;
pq.fetch_all(pool).await
} So, what happens is:
The pattern would work well for cases where you don't want to change the columns, only apply refinement operations to the return dataset. This covers the main use cases - The full dynamic query cannot be checked at compile time, but that is probably acceptable given the explosion problem mentioned above. |
This is excellent, thank you! Two small points:
|
Introduction
Dynamic queries are a pretty common usecase, for example when filtering through records.
Here are some workarounds which have been used so far:
1. checking for
NULL
first2. writing the query out multiple times
3. ditching compile-time checking
Proposal
I propose the addition of a new macro, which automates workaround 2.
The macro would accept a much richer syntax compared to
query!
orquery_as!
, allowing the use ofif
andmatch
.A call would then expand to
query!
/query_as!
calls for every permutation of the query, allowing all of them to be checked against the database at compile-time.I attempted to implement this before here [example] [implementation] using
macro_rules!
.While it does work, it has some heavy limitations, which often came up in the
#ormx
channel on discord.A new implementation I started working uses a procedural macro and doesn't suffer from these limitations.
Here is an example of how using it currently looks like. The exact syntax is of course up for debate.
This currently expands to this:
Issues
With this approach, each generated call to
query_as!
must result in the same type.Currently, this is solved by generating an enum,
ConditionalMap8
in this case, which has 8 variants. Each of those variants wraps asqlx::query::Map
. Calling a function likefetch_all
onConditionalMap8
then does amatch
and redirects the call to the appropriateMap
.For something like
conditional_query!
(a conditional verison ofquery!
), this approach is currently not possible since each call toquery!
returns aMap
with an ad-hoc generated anonymous type. For this to work withquery!
, it would be necessarry that sqlx only ever generated one struct for a given set of columns such that the different calls toquery!
with slightly different queries would returnMap
with the same generated type.@mehcode suggested a different approach - sqlx could just expose a macro to generate a struct for a given query. Then,
conditional_query!
could just be implemented as a wrapper around this macro andconditional_query_as!
.The text was updated successfully, but these errors were encountered: