GORM Playground Link

https://github.com/go-gorm/playground/pull/689

Description

It looks like something in v.1.25.6 changed / broke, how SQL (at least for Postgres) is generated. Especially in the case of nested WHERE (+ JOIN?) like shown below. It was the smallest example I could create, but maybe it also occures only with nested WHERE?

As this changes the result completely and outputing simply all results, this could be a critical thing. We use this kind of query to combine search-text with user permissions, and in this case it makes user permissions completely ignored through the first users.name ILIKE '%%' OR. Notice the additional users.name ILIKE '%%' OR companies.name ILIKE '%%' AND.

Copies from playground PR:

query := DB.Table("users").
        Select("companies.country", "COUNT(DISTINCT users.name)").
        Joins("LEFT JOIN companies ON users.company_id = companies.id").
        Group("companies.country")
    query.Where(query.Where("users.name ILIKE ?", "%%").
        Or("companies.name ILIKE ?", "%%"))
    query.Where("users.something IN (?)", []string{"1"})

Query v.1.25.5:

SELECT companies.country, COUNT(DISTINCT users.name)
FROM "users"
         LEFT JOIN companies ON users.company_id = companies.id
WHERE (users.name ILIKE '%%' OR companies.name ILIKE '%%')
  AND users.something IN ('1')
GROUP BY "companies"."country";

Result: [{DE 1} {GB 1}]

Query v1.25.6+:

SELECT companies.country, COUNT(DISTINCT users.name)
FROM "users"
         LEFT JOIN companies ON users.company_id = companies.id
WHERE users.name ILIKE '%%'
   OR companies.name ILIKE '%%' AND (users.name ILIKE '%%' OR companies.name ILIKE '%%') AND users.something IN ('1')
GROUP BY "companies"."country";

Result: [{DE 2} {GB 1} {US 1}]

I was not able to track this issue down to a specific change. If I can further support, please get back

Comment From: janisz

I think I found a broken generation in different part as well. In my case migration is not working due to broken sql that uses $2 but not $1 resulting in insufficient arguments - https://github.com/stackrox/stackrox/pull/9921

https://github.com/stackrox/stackrox/actions/runs/7900053395?pr=9921#summary-21560778111

Comment From: urashidmalik

existing unique keys are also broken reverting back to 1.25.5 works

Comment From: a631807682

refer to https://gorm.io/docs/method_chaining.html#Reusability-and-Safety

query := DB.Table("users").
        Select("companies.country", "COUNT(DISTINCT users.name)").
        Joins("LEFT JOIN companies ON users.company_id = companies.id").
        Group("companies.country")
-   query.Where(query.Where("users.name ILIKE ?", "%%").
+   query.Where(query.Session(&gorm.Session{}).Where("users.name ILIKE ?", "%%").
        Or("companies.name ILIKE ?", "%%"))
    query.Where("users.something IN (?)", []string{"1"})

Comment From: chbiel

@a631807682 although that seem to fix it, the documentation at https://gorm.io/docs/advanced_query.html#Group-Conditions clearly says that the way without query.Session(&gorm.Session{}) should work as well.

further the documentation you linked is about "reusing queries", what that does not happen here. We have one query with different sub conditions only split into different calls, to make it more readable (or in our concrete case have a function that adds the Where conditions) and as linked above, that should not break.

Comment From: chbiel

@jinzhu as the issue is now open for nearly two weeks, do you have an idea what broke that behaviour? or do you at least agree that it's a critical bug?

Comment From: a631807682

@a631807682 although that seem to fix it, the documentation at gorm.io/docs/advanced_query.html#Group-Conditions clearly says that the way without query.Session(&gorm.Session{}) should work as well.

further the documentation you linked is about "reusing queries", what that does not happen here. We have one query with different sub conditions only split into different calls, to make it more readable (or in our concrete case have a function that adds the Where conditions) and as linked above, that should not break.

This is as expected, for more detailed information refer to https://github.com/go-gorm/gorm/issues/6811#issuecomment-1916381803 https://github.com/go-gorm/gorm/issues/5182#issuecomment-1075953290

Comment From: chbiel

Hmm. I still don't agree. The documentation https://gorm.io/docs/advanced_query.html#Group-Conditions generates (at least in the code outline) the correct SQL.

Or is your point about the separation?

query := DB.Table("users").Where("users.something IN (?)", []string{"1"})
query.Where(query.Where("users.name ILIKE ?", "%%").Or("companies.name ILIKE ?", "%%"))

vs.

query := DB.Table("users").Where("users.something IN (?)", []string{"1"}).Where(query.Where("users.name ILIKE ?", "%%").Or("companies.name ILIKE ?", "%%"))

Comment From: a631807682

The chainable api does not change the DB instance, it returns a new copy. But query, as a new copy, will be changed by chainable api.

// query change by nested where
query.Where(query.Where("users.name ILIKE ?", "%%").Or("companies.name ILIKE ?", "%%"))
// query or DB will not change
query.Where(DB.Where("users.name ILIKE ?", "%%").Or("companies.name ILIKE ?", "%%"))

Comment From: chbiel

OK I understand, thanks for clearification.

But I would still say, that the documentation at https://gorm.io/docs/advanced_query.html#Group-Conditions is not correct about that, as it uses lowercase db suggesting it is a local variable that is reused here (like we used it).

So the output of

db.Where(
  db.Where("pizza = ?", "pepperoni").Where(db.Where("size = ?", "small").Or("size = ?", "medium")),
).Or(
  db.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"),
).Find(&Pizza{})

is not

SELECT *
FROM `pizzas`
WHERE (pizza = "pepperoni" AND (size = "small" OR size = "medium")) OR (pizza = "hawaiian" AND size = "xlarge")

but

SELECT *
FROM `pizzas`
WHERE (pizza = "hawaiian" AND size = "xlarge") AND -- db.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge")
    (size = "small" OR size = "medium") AND -- db.Where("size = ?", "small").Or("size = ?", "medium")
    (pizza = "pepperoni" AND (size = "small" OR size = "medium")) AND -- db.Where("pizza = ?", "pepperoni").Where(db.Where("size = ?", "small").Or("size = ?", "medium")),
    ((pizza = "pepperoni" AND (size = "small" OR size = "medium")) OR (pizza = "hawaiian" AND size = "xlarge")) -- all combined

because the inner db.Wheres also modify the query directly.

To get the SQL from the example, it would need to be

db.Where(
  DB.Where("pizza = ?", "pepperoni").Where(DB.Where("size = ?", "small").Or("size = ?", "medium")),
).Or(
  DB.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"),
).Find(&Pizza{})

Or do I still not get something here?