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

Missing parenthesis lead to broken SQL queries #2140

Closed
2 tasks done
DNNX opened this issue Aug 8, 2019 · 7 comments
Closed
2 tasks done

Missing parenthesis lead to broken SQL queries #2140

DNNX opened this issue Aug 8, 2019 · 7 comments
Milestone

Comments

@DNNX
Copy link

DNNX commented Aug 8, 2019

Setup

Versions

  • Rust: rustc 1.36.0 (a53f9df32 2019-07-03)
  • Diesel: latest master (0cacfa0)
  • Database: psql (11.1, server 11.4 (Debian 11.4-1.pgdg90+1))
  • Operating System Debian 11.4-1.pgdg90+1

Feature Flags

  • diesel: postgres

Problem Description

Missing parentheses around equality operator lead to malformed SQL queries in the best case and wrong SQL queries silently returning incorrect results in the worst case.

What are you trying to accomplish?

Build a query which corresponds to something like this:

SELECT (1 > 0) = true

Translating this to diesel, the code will look like this:

numbers.select((n.gt(0)).eq(true));

Full runnable test can be found in my operator-precedence-bug branch of diesel. Here is the full test DNNX/diesel@master...operator-precedence-bug which can be run with (cd diesel_tests && cargo test --features "postgres" --no-default-features expressions::test_operator_precedence).

What is the expected output?

Green test.

What is the actual output?

➜  diesel git:(operator-precedence-bug) ✗ (cd diesel_tests && cargo test --features "postgres" --no-default-features expressions::test_operator_precedence)
   Compiling diesel_tests v0.1.0 (/Users/dnnx/work/diesel/diesel_tests)
    Finished dev [unoptimized + debuginfo] target(s) in 12.26s
     Running /Users/dnnx/work/diesel/target/debug/deps/integration_tests-514980afda54a7bd

running 1 test
test expressions::test_operator_precedence ... FAILED

failures:

---- expressions::test_operator_precedence stdout ----
thread 'expressions::test_operator_precedence' panicked at 'assertion failed: `(left == right)`
  left: `Ok(true)`,
 right: `Err(DatabaseError(__Unknown, "syntax error at or near \"=\""))`', diesel_tests/tests/expressions/mod.rs:469:5
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.


failures:
    expressions::test_operator_precedence

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 309 filtered out

error: test failed, to rerun pass '--test integration_tests'

Are you seeing any additional errors?

No.

Steps to reproduce

  1. Check our my branch or apply the diff below to latest diesel master.
  2. Run `(cd diesel_tests && cargo test --features "postgres" --no-default-features expressions::test_operator_precedence).
diff --git a/diesel_tests/tests/expressions/mod.rs b/diesel_tests/tests/expressions/mod.rs
index e6cd0b1e..2dbd584c 100644
--- a/diesel_tests/tests/expressions/mod.rs
+++ b/diesel_tests/tests/expressions/mod.rs
@@ -454,3 +454,17 @@ fn test_arrays_b() {
 
     assert_eq!(value, vec![7, 14]);
 }
+
+#[test]
+fn test_operator_precedence() {
+    use self::numbers::columns::*;
+    use self::numbers::table as numbers;
+
+    let connection = connection();
+    connection
+        .execute("INSERT INTO numbers (n) VALUES (2)")
+        .unwrap();
+    let source = numbers.select((n.gt(0)).eq(true));
+
+    assert_eq!(Ok(true), source.first(&connection));
+}

Additional notes

I already raised this issue in #2133 (comment), but I probably failed to express myself very clearly.

The big problem with this bug is that it may result in incorrectly working SQL queries, in particular in Mysql. Mysql is less strict about queries, so it parses things like select 2=2=2=2 just fine, whereas Postgres rejects such queries.

I tried to fix the bug myself, but I don't even know where to start. I think adding parentheses around every operator might be a solution, but there's one subtle issue with it: diesel also uses eq to generate SET part of the UPDATE statements - and there having parentheses around x = <expr> in UPDATE foo SET x = <expr> will be a problem.

Checklist

  • I have already looked over the issue tracker for similar issues.
  • This issue can be reproduced on Rust's stable channel. (Your issue will be
    closed if this is not the case)
@weiznich
Copy link
Member

weiznich commented Aug 8, 2019

To be clear numbers.select((n.gt(0)).eq(true)); is never to be expected to insert any parenthesis based on parenthesis on the rust code because there is no way to tell at a type system level (that's where the query string is build) if there are parenthesis.

Beside of that I see two possible solutions to fix this:

  1. Being smarter about where the insert parenthesis in the generated SQL
  2. Expose a function to explicitly group query part and document that this should be used if required.

As you've already noticed it is not as simple as adding just parenthesis everywhere the first solution would involve a quite heavy rework of our internal query constructing workflow. Additionally even than there may be expressions where it is just not decidable which variant is the right one. In that light I'm in favour of keeping the rules as simple as possible and just add parenthesis in as few cases as possible and expose the functionality to insert parenthesis manually.

@DNNX
Copy link
Author

DNNX commented Aug 8, 2019

To be clear numbers.select((n.gt(0)).eq(true)); is never to be expected to insert any parenthesis based on parenthesis on the rust code because there is no way to tell at a type system level (that's where the query string is build) if there are parenthesis.

Yes, I understand that.

... Additionally even than there may be expressions where it is just not decidable which variant is the right one.

Could you please give an example? From what I can see, if we just wrap all binary operations in parenthesis, the resulting SQL AST will be exactly the same as the original Rust AST (which is what library user want, as I see it). Considering the example of n.eq(0).eq(true), syntax tree would be something like:

           Op =
         /      \
       Op =      Const true
    /       \
Var n       Const 0

And, assuming we put parenthesis around every binary operation, SQL syntax tree is exactly the same. It will result in (n = 0) = true then, which is semantically equal to the Rust one.

As to using eq in UPDATE ... SET (as in update(posts).set(body.eq("updated")), I don't really understand why the same eq function is used there as in boolean-returning equality expression. Those are kind of different things. = in UPDATE doesn't return anything - it's not an expression; comparison operator = returns a bool. Also, right hand side of = in UPDATE can't be an arbitrary expression, unlike in equality comparison operator. It's just a coincidence that they both syntactically represented as equal sign. Assignment operator could as well be := or <- or something along those lines, and equality operator could easily be ==.

It may sound stupid or maybe overly naïve, but what if we change the public api of update/set to look like this:

update(posts).set(body, "updated");
update(users).set(login_attempts, login_attempts + 1);

// or

update(posts).set(body.assign("updated"));

This way we could freely add parenthesis around all binary operators and be sure that Rust and SQL expressions have the same meaning.

@weiznich
Copy link
Member

weiznich commented Aug 8, 2019

Could you please give an example?

What about the following, pathological example?

true.into_sql::<Bool>.eq(false).eq(false)

That would generate the following query true = false = flase where both variants ((true = false) = false and true = (false = false) would be meaningful (depending on the context and the definition of meaningful, but it's just an example 🤷‍)

It may sound stupid or maybe overly naïve, but what if we change the public api of update/set to look like this:

Changing the public API of update/set is currently not an option because of our backwards compatibility promise.

@DNNX
Copy link
Author

DNNX commented Aug 9, 2019

true.into_sql::<Bool>.eq(false).eq(false)

Well I think syntactically this is the tree below, no ambiguity here. So if we properly wrap every expression in brackets, it will render as ((true = false) = false).

           Op =
         /      \
       Op =      Const false
    /       \
Var true       Const false

If one wants (true = (false = false)) to be generated, they have to write corresponding Rust: true.into_sql::<Bool>.eq(false.into_sql::<Bool>.eq(true)). Again, clear 1-1 mapping between Rust code and generated SQL.

Changing the public API of update/set is currently not an option because of our backwards compatibility promise.

Makes total sense.

Speaking of keeping backward compatibility. I was also playing with the idea of changing the way SET expression is rendered. Let's assume all operations are generated with parenthesis, no exceptions. Then in order to "fix" UPDATE...SET, we can implement a new struct Ungrouped, which will "override" walk_ast implementation of given binary operator not to render () around the expression.

We can use it on one single place then, like this:

diff --git a/diesel/src/query_builder/update_statement/mod.rs b/diesel/src/query_builder/update_statement/mod.rs
index 53ec7870..faeb7c70 100644
--- a/diesel/src/query_builder/update_statement/mod.rs
+++ b/diesel/src/query_builder/update_statement/mod.rs
@@ -207,7 +207,7 @@ where
         out.push_sql("UPDATE ");
         self.table.from_clause().walk_ast(out.reborrow())?;
         out.push_sql(" SET ");
-        self.values.walk_ast(out.reborrow())?;
+        Ungrouped(self.values).walk_ast(out.reborrow())?;
         self.where_clause.walk_ast(out.reborrow())?;
         self.returning.walk_ast(out.reborrow())?;
         Ok(())

Hmmm, actually, looking at the code, I'm not even sure anymore that update(t).set(x.eq(1)) would generate brackets around x = 1 even if we override Eq to generate parenthesis no matter what. Looks like my assumption was wrong. I think, this code is used for SET ... rendering: https://github.com/diesel-rs/diesel/blob/master/diesel/src/query_builder/update_statement/changeset.rs#L72-L74, which is independent from the code which renders x.eq(y) when they are part of equality comparison expressions.

So, bottom line is: I still believe that the right way to go is to wrap all operator expressions in parenthesis there

$left;
$op;
$right;
.

When I have more time, I'll play with the code. I want to see what is going to be broken by this change as I don't really see it now.

@DNNX
Copy link
Author

DNNX commented Aug 9, 2019

I just made a little test and ran (cd diesel_tests && RUST_TEST_THREADS=1 cargo test --no-default-features --features mysql) (and --features posgres too) on this DNNX/diesel@master...brackets-brackets-everywhere.

Almost everything passed with one exception: between tests. Because of the way Between/NotBetween structs are implemented, the And part is assumed to be a subexpression and it renders it as (x BETWEEN (y AND z)). But conceptually, BETWEEN is not a binary operator, it's a ternary construct. If we implement it this way, as a struct with 3 fields, and implement QueryFragment for it to avoid rendering inner parenthesis, everything is going to be green, probably :)

@weiznich
Copy link
Member

First of all: Passing all tests in diesel_tests does not mean that there is no code out there that will break by this change.
Beside of that I'm not sure if that follows the operator precedence from the underlying database systems. I would like to avoid situations where diesels generated sql diverges from those rules. (I cannot point out a example where this is the case, by this is something that needs to be verified.)

Additionally I would like to hear the opinion of the other @diesel-rs/core members here.
Maybe open a PR and continue the discussion there?

@sgrif
Copy link
Member

sgrif commented Aug 14, 2019

@weiznich This seems like the same issue as #2017, doesn't it? The same solution seems like it'd make sense IMO. I'm wondering if we should even just always insert parens for all uses of infix_operator!

@weiznich weiznich added this to the 2.0 milestone Jun 21, 2020
weiznich added a commit to weiznich/diesel that referenced this issue Sep 28, 2020
Include parenthesis around every infix operation.

This requires making `BETWEEN` and `NOT BETWEEN` manually implemented
query nodes instead of relaying on that ones defined by
`infix_operator!` because `x BETWEEN (l AND u)` is invalid SQL.
weiznich added a commit to weiznich/diesel that referenced this issue Oct 2, 2020
Include parenthesis around every infix operation provided by one of the
`ExpressionMethod` traits.

It is not possible to include this directly into the `infix_operator!`
macro due several SQL constructs:

* `a LIKE b` can have optional `ESCAPE` clauses (same for `NOT LIKE`,
`ILIKE` eg). Unconditionally putting a parenthesis around `(a LIKE b)`
is no valid SQL
* `a BETWEEN l AND u` is implemented a set of two infix operators,
`BETWEEN` and `AND`, so putting the parenthesis directly into the
operator macro would lead to `x BETWEEN (l AND u)` which is not valid
SQL

Instead I've opted for adding explicit parenthesis in the corresponding
`ExpressionMethod` (and similar traits` implementations. As part of this
change I've changed all the return types there to use some type from
`diesel::dsl`, so that potential users have an easier time to figure out
what types to use if they have to write some where clause or something
like that.

While changing the return types to `diesel::dsl` types I noticed  and
fixed the following issues:

* Not all methods had an equivalent type in `diesel::dsl`, sometimes
because it was missing, sometimes because the postgres specific types
weren't correctly exported there. Both is fixed now
* Some types were not anymore compatible with the actual methods. A
notable example is `dsl::And` and `BoolExpressionMethods::and` where the
later returned a potentially nullable type depending on the input types,
while the first was only valid for not nullable right sides. Fixing that
was a bit more complicated:
    * I've opted for making the return type of `BoolExpressionMethods::and`
      (and `or`) unconditonally nullable. This prevents having an explicit
     `ST` parameter on `dsl::And`
    * This in turn requires that we accept a non nullable expression in
      places where a nullable expression is expected. Technically this
      was already possible, but would require explicitly calling
      `.nullable()` on affected expressions. Given that `.and()` and
      `.or()` are really common, that seemed not to be a good solution.
      As result I've changed the `AsExpression` impl for `T: Expression`
      in such a way that for any `T` with `T::SqlType: SqlType<IsNull =
      is_nullable::NotNull>` there is now an impl
      `AsExpression<Nullable<T::SqlType>>` so that such expressions are
      also accepted in nullable contexts.
weiznich added a commit to weiznich/diesel that referenced this issue Oct 2, 2020
Include parenthesis around every infix operation provided by one of the
`ExpressionMethod` traits.

It is not possible to include this directly into the `infix_operator!`
macro due several SQL constructs:

* `a LIKE b` can have optional `ESCAPE` clauses (same for `NOT LIKE`,
`ILIKE` eg). Unconditionally putting a parenthesis around `(a LIKE b)`
is no valid SQL
* `a BETWEEN l AND u` is implemented a set of two infix operators,
`BETWEEN` and `AND`, so putting the parenthesis directly into the
operator macro would lead to `x BETWEEN (l AND u)` which is not valid
SQL

Instead I've opted for adding explicit parenthesis in the corresponding
`ExpressionMethod` (and similar traits` implementations. As part of this
change I've changed all the return types there to use some type from
`diesel::dsl`, so that potential users have an easier time to figure out
what types to use if they have to write some where clause or something
like that.

While changing the return types to `diesel::dsl` types I noticed  and
fixed the following issues:

* Not all methods had an equivalent type in `diesel::dsl`, sometimes
because it was missing, sometimes because the postgres specific types
weren't correctly exported there. Both is fixed now
* Some types were not anymore compatible with the actual methods. A
notable example is `dsl::And` and `BoolExpressionMethods::and` where the
later returned a potentially nullable type depending on the input types,
while the first was only valid for not nullable right sides. Fixing that
was a bit more complicated:
    * I've opted for making the return type of `BoolExpressionMethods::and`
      (and `or`) unconditonally nullable. This prevents having an explicit
     `ST` parameter on `dsl::And`
    * This in turn requires that we accept a non nullable expression in
      places where a nullable expression is expected. Technically this
      was already possible, but would require explicitly calling
      `.nullable()` on affected expressions. Given that `.and()` and
      `.or()` are really common, that seemed not to be a good solution.
      As result I've changed the `AsExpression` impl for `T: Expression`
      in such a way that for any `T` with `T::SqlType: SqlType<IsNull =
      is_nullable::NotNull>` there is now an impl
      `AsExpression<Nullable<T::SqlType>>` so that such expressions are
      also accepted in nullable contexts.
weiznich added a commit to weiznich/diesel that referenced this issue Oct 2, 2020
Include parenthesis around every infix operation provided by one of the
`ExpressionMethod` traits.

It is not possible to include this directly into the `infix_operator!`
macro due several SQL constructs:

* `a LIKE b` can have optional `ESCAPE` clauses (same for `NOT LIKE`,
`ILIKE` eg). Unconditionally putting a parenthesis around `(a LIKE b)`
is no valid SQL
* `a BETWEEN l AND u` is implemented a set of two infix operators,
`BETWEEN` and `AND`, so putting the parenthesis directly into the
operator macro would lead to `x BETWEEN (l AND u)` which is not valid
SQL

Instead I've opted for adding explicit parenthesis in the corresponding
`ExpressionMethod` (and similar traits` implementations. As part of this
change I've changed all the return types there to use some type from
`diesel::dsl`, so that potential users have an easier time to figure out
what types to use if they have to write some where clause or something
like that.

While changing the return types to `diesel::dsl` types I noticed  and
fixed the following issues:

* Not all methods had an equivalent type in `diesel::dsl`, sometimes
because it was missing, sometimes because the postgres specific types
weren't correctly exported there. Both is fixed now
* Some types were not anymore compatible with the actual methods. A
notable example is `dsl::And` and `BoolExpressionMethods::and` where the
later returned a potentially nullable type depending on the input types,
while the first was only valid for not nullable right sides. Fixing that
was a bit more complicated:
    * I've opted for making the return type of `BoolExpressionMethods::and`
      (and `or`) unconditonally nullable. This prevents having an explicit
     `ST` parameter on `dsl::And`
    * This in turn requires that we accept a non nullable expression in
      places where a nullable expression is expected. Technically this
      was already possible, but would require explicitly calling
      `.nullable()` on affected expressions. Given that `.and()` and
      `.or()` are really common, that seemed not to be a good solution.
      As result I've changed the `AsExpression` impl for `T: Expression`
      in such a way that for any `T` with `T::SqlType: SqlType<IsNull =
      is_nullable::NotNull>` there is now an impl
      `AsExpression<Nullable<T::SqlType>>` so that such expressions are
      also accepted in nullable contexts.
weiznich added a commit to weiznich/diesel that referenced this issue Oct 2, 2020
Include parenthesis around every infix operation provided by one of the
`ExpressionMethod` traits.

It is not possible to include this directly into the `infix_operator!`
macro due several SQL constructs:

* `a LIKE b` can have optional `ESCAPE` clauses (same for `NOT LIKE`,
`ILIKE` eg). Unconditionally putting a parenthesis around `(a LIKE b)`
is no valid SQL
* `a BETWEEN l AND u` is implemented a set of two infix operators,
`BETWEEN` and `AND`, so putting the parenthesis directly into the
operator macro would lead to `x BETWEEN (l AND u)` which is not valid
SQL

Instead I've opted for adding explicit parenthesis in the corresponding
`ExpressionMethod` (and similar traits` implementations. As part of this
change I've changed all the return types there to use some type from
`diesel::dsl`, so that potential users have an easier time to figure out
what types to use if they have to write some where clause or something
like that.

While changing the return types to `diesel::dsl` types I noticed  and
fixed the following issues:

* Not all methods had an equivalent type in `diesel::dsl`, sometimes
because it was missing, sometimes because the postgres specific types
weren't correctly exported there. Both is fixed now
* Some types were not anymore compatible with the actual methods. A
notable example is `dsl::And` and `BoolExpressionMethods::and` where the
later returned a potentially nullable type depending on the input types,
while the first was only valid for not nullable right sides. Fixing that
was a bit more complicated:
    * I've opted for making the return type of `BoolExpressionMethods::and`
      (and `or`) unconditonally nullable. This prevents having an explicit
     `ST` parameter on `dsl::And`
    * This in turn requires that we accept a non nullable expression in
      places where a nullable expression is expected. Technically this
      was already possible, but would require explicitly calling
      `.nullable()` on affected expressions. Given that `.and()` and
      `.or()` are really common, that seemed not to be a good solution.
      As result I've changed the `AsExpression` impl for `T: Expression`
      in such a way that for any `T` with `T::SqlType: SqlType<IsNull =
      is_nullable::NotNull>` there is now an impl
      `AsExpression<Nullable<T::SqlType>>` so that such expressions are
      also accepted in nullable contexts.
weiznich added a commit to weiznich/diesel that referenced this issue Oct 2, 2020
Include parenthesis around every infix operation provided by one of the
`ExpressionMethod` traits.

It is not possible to include this directly into the `infix_operator!`
macro due several SQL constructs:

* `a LIKE b` can have optional `ESCAPE` clauses (same for `NOT LIKE`,
`ILIKE` eg). Unconditionally putting a parenthesis around `(a LIKE b)`
is no valid SQL
* `a BETWEEN l AND u` is implemented a set of two infix operators,
`BETWEEN` and `AND`, so putting the parenthesis directly into the
operator macro would lead to `x BETWEEN (l AND u)` which is not valid
SQL

Instead I've opted for adding explicit parenthesis in the corresponding
`ExpressionMethod` (and similar traits` implementations. As part of this
change I've changed all the return types there to use some type from
`diesel::dsl`, so that potential users have an easier time to figure out
what types to use if they have to write some where clause or something
like that.

While changing the return types to `diesel::dsl` types I noticed  and
fixed the following issues:

* Not all methods had an equivalent type in `diesel::dsl`, sometimes
because it was missing, sometimes because the postgres specific types
weren't correctly exported there. Both is fixed now
* Some types were not anymore compatible with the actual methods. A
notable example is `dsl::And` and `BoolExpressionMethods::and` where the
later returned a potentially nullable type depending on the input types,
while the first was only valid for not nullable right sides. Fixing that
was a bit more complicated:
    * I've opted for making the return type of `BoolExpressionMethods::and`
      (and `or`) unconditonally nullable. This prevents having an explicit
     `ST` parameter on `dsl::And`
    * This in turn requires that we accept a non nullable expression in
      places where a nullable expression is expected. Technically this
      was already possible, but would require explicitly calling
      `.nullable()` on affected expressions. Given that `.and()` and
      `.or()` are really common, that seemed not to be a good solution.
      As result I've changed the `AsExpression` impl for `T: Expression`
      in such a way that for any `T` with `T::SqlType: SqlType<IsNull =
      is_nullable::NotNull>` there is now an impl
      `AsExpression<Nullable<T::SqlType>>` so that such expressions are
      also accepted in nullable contexts.
weiznich added a commit to weiznich/diesel that referenced this issue Oct 5, 2020
Include parenthesis around every infix operation provided by one of the
`ExpressionMethod` traits.

It is not possible to include this directly into the `infix_operator!`
macro due several SQL constructs:

* `a LIKE b` can have optional `ESCAPE` clauses (same for `NOT LIKE`,
`ILIKE` eg). Unconditionally putting a parenthesis around `(a LIKE b)`
is no valid SQL
* `a BETWEEN l AND u` is implemented a set of two infix operators,
`BETWEEN` and `AND`, so putting the parenthesis directly into the
operator macro would lead to `x BETWEEN (l AND u)` which is not valid
SQL

Instead I've opted for adding explicit parenthesis in the corresponding
`ExpressionMethod` (and similar traits` implementations. As part of this
change I've changed all the return types there to use some type from
`diesel::dsl`, so that potential users have an easier time to figure out
what types to use if they have to write some where clause or something
like that.

While changing the return types to `diesel::dsl` types I noticed  and
fixed the following issues:

* Not all methods had an equivalent type in `diesel::dsl`, sometimes
because it was missing, sometimes because the postgres specific types
weren't correctly exported there. Both is fixed now
* Some types were not anymore compatible with the actual methods. A
notable example is `dsl::And` and `BoolExpressionMethods::and` where the
later returned a potentially nullable type depending on the input types,
while the first was only valid for not nullable right sides. Fixing that
was a bit more complicated:
    * I've opted for making the return type of `BoolExpressionMethods::and`
      (and `or`) unconditonally nullable. This prevents having an explicit
     `ST` parameter on `dsl::And`
    * This in turn requires that we accept a non nullable expression in
      places where a nullable expression is expected. Technically this
      was already possible, but would require explicitly calling
      `.nullable()` on affected expressions. Given that `.and()` and
      `.or()` are really common, that seemed not to be a good solution.
      As result I've changed the `AsExpression` impl for `T: Expression`
      in such a way that for any `T` with `T::SqlType: SqlType<IsNull =
      is_nullable::NotNull>` there is now an impl
      `AsExpression<Nullable<T::SqlType>>` so that such expressions are
      also accepted in nullable contexts.
weiznich added a commit that referenced this issue Oct 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants