Repository: go-gorm/gorm (go)

Issue:
db.Not method when used with multiple Where conditions has changed unexpectedly 
## GORM Playground Link

- https://github.com/go-gorm/playground/pull/695
- https://github.com/go-gorm/playground/pull/696

## Description

A regression introduced in Gorm v1.25.6, specifically in commit https://github.com/go-gorm/gorm/commit/940358e0ddd1e6d5e893184943a4fa71a0c28521.

The behavior of the db.Not method when used with multiple Where conditions has changed unexpectedly. Prior to v1.25.6, the query `db.Not(db.Where("a=1").Where("b=1"))` would correctly translate to SQL as `NOT (a=1 AND b=1)`. However, after the specified commit, the same query started translating to `(NOT a=1 AND NOT b=1)`, which is a significant alteration in the logic.

I created PR (https://github.com/go-gorm/gorm/pull/6844) to address this issue.

<!-- Your use case -->


Pull request description:
Fix regression in db.Not introduced in v1.25.6.
<!--
Make sure these boxes checked before submitting your pull request.

For significant changes, please open an issue to make an agreement on an implementation design/plan first before starting it.
-->

- [x] Do only one thing
- [x] Non breaking API changes
- [x] Tested

### What did this pull request do?

Fix #6845

Address a regression introduced in Gorm v1.25.6, specifically in commit 940358e.

The behavior of the `db.Not` method when used with multiple `Where` conditions has changed unexpectedly. Prior to v1.25.6, the query `db.Not(db.Where("a=1").Where("b=1"))` would correctly translate to SQL as `NOT (a=1 AND b=1)`. However, after the specified commit, the same query started translating to `(NOT a=1 AND NOT b=1)`, which is a significant alteration in the logic.

This PR reverts the `db.Not` method's behavior back to its original state when *gorm.DB passed as arguments.


### User Case Description

<!-- Your use case -->


Patch:
diff --git a/clause/where.go b/clause/where.go
index 46d0b31..9ac7857 100644
--- a/clause/where.go
+++ b/clause/where.go
@@ -21,11 +21,11 @@ func (where Where) Name() string {
 
 // Build build where clause
 func (where Where) Build(builder Builder) {
-    if len(where.Exprs) == 1 {
-        if andCondition, ok := where.Exprs[0].(AndConditions); ok {
-           where.Exprs = andCondition.Exprs
-        }
-    }
+	if len(where.Exprs) == 1 {
+		if andCondition, ok := where.Exprs[0].(AndConditions); ok {
+			where.Exprs = andCondition.Exprs
+		}
+	}
 
 	// Switch position if the first query expression is a single Or condition
 	for idx, expr := range where.Exprs {
@@ -166,19 +166,58 @@ type NotConditions struct {
 }
 
 func (not NotConditions) Build(builder Builder) {
-	if len(not.Exprs) > 1 {
-		builder.WriteByte('(')
+	anyNegationBuilder := false
+	for _, c := range not.Exprs {
+		if _, ok := c.(NegationExpressionBuilder); ok {
+			anyNegationBuilder = true
+			break
+		}
 	}
 
-	for idx, c := range not.Exprs {
-		if idx > 0 {
-			builder.WriteString(AndWithSpace)
+	if anyNegationBuilder {
+		if len(not.Exprs) > 1 {
+			builder.WriteByte('(')
 		}
 
-		if negationBuilder, ok := c.(NegationExpressionBuilder); ok {
-			negationBuilder.NegationBuild(builder)
-		} else {
-			builder.WriteString("NOT ")
+		for idx, c := range not.Exprs {
+			if idx > 0 {
+				builder.WriteString(AndWithSpace)
+			}
+
+			if negationBuilder, ok := c.(NegationExpressionBuilder); ok {
+				negationBuilder.NegationBuild(builder)
+			} else {
+				builder.WriteString("NOT ")
+				e, wrapInParentheses := c.(Expr)
+				if wrapInParentheses {
+					sql := strings.ToUpper(e.SQL)
+					if wrapInParentheses = strings.Contains(sql, AndWithSpace) || strings.Contains(sql, OrWithSpace); wrapInParentheses {
+						builder.WriteByte('(')
+					}
+				}
+
+				c.Build(builder)
+
+				if wrapInParentheses {
+					builder.WriteByte(')')
+				}
+			}
+		}
+
+		if len(not.Exprs) > 1 {
+			builder.WriteByte(')')
+		}
+	} else {
+		builder.WriteString("NOT ")
+		if len(not.Exprs) > 1 {
+			builder.WriteByte('(')
+		}
+
+		for idx, c := range not.Exprs {
+			if idx > 0 {
+				builder.WriteString(AndWithSpace)
+			}
+
 			e, wrapInParentheses := c.(Expr)
 			if wrapInParentheses {
 				sql := strings.ToUpper(e.SQL)
@@ -193,9 +232,9 @@ func (not NotConditions) Build(builder Builder) {
 				builder.WriteByte(')')
 			}
 		}
-	}
 
-	if len(not.Exprs) > 1 {
-		builder.WriteByte(')')
+		if len(not.Exprs) > 1 {
+			builder.WriteByte(')')
+		}
 	}
 }


Test patch:
diff --git a/callbacks/helper_test.go b/callbacks/helper_test.go
new file mode 100644
index 0000000..08f94e2
--- /dev/null
+++ b/callbacks/helper_test.go
@@ -0,0 +1,157 @@
+package callbacks
+
+import (
+	"reflect"
+	"testing"
+
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+)
+
+func TestLoadOrStoreVisitMap(t *testing.T) {
+	var vm visitMap
+	var loaded bool
+	type testM struct {
+		Name string
+	}
+
+	t1 := testM{Name: "t1"}
+	t2 := testM{Name: "t2"}
+	t3 := testM{Name: "t3"}
+
+	vm = make(visitMap)
+	if loaded = loadOrStoreVisitMap(&vm, reflect.ValueOf(&t1)); loaded {
+		t.Fatalf("loaded should be false")
+	}
+
+	if loaded = loadOrStoreVisitMap(&vm, reflect.ValueOf(&t1)); !loaded {
+		t.Fatalf("loaded should be true")
+	}
+
+	// t1 already exist but t2 not
+	if loaded = loadOrStoreVisitMap(&vm, reflect.ValueOf([]*testM{&t1, &t2, &t3})); loaded {
+		t.Fatalf("loaded should be false")
+	}
+
+	if loaded = loadOrStoreVisitMap(&vm, reflect.ValueOf([]*testM{&t2, &t3})); !loaded {
+		t.Fatalf("loaded should be true")
+	}
+}
+
+func TestConvertMapToValuesForCreate(t *testing.T) {
+	testCase := []struct {
+		name   string
+		input  map[string]interface{}
+		expect clause.Values
+	}{
+		{
+			name: "Test convert string value",
+			input: map[string]interface{}{
+				"name": "my name",
+			},
+			expect: clause.Values{
+				Columns: []clause.Column{{Name: "name"}},
+				Values:  [][]interface{}{{"my name"}},
+			},
+		},
+		{
+			name: "Test convert int value",
+			input: map[string]interface{}{
+				"age": 18,
+			},
+			expect: clause.Values{
+				Columns: []clause.Column{{Name: "age"}},
+				Values:  [][]interface{}{{18}},
+			},
+		},
+		{
+			name: "Test convert float value",
+			input: map[string]interface{}{
+				"score": 99.5,
+			},
+			expect: clause.Values{
+				Columns: []clause.Column{{Name: "score"}},
+				Values:  [][]interface{}{{99.5}},
+			},
+		},
+		{
+			name: "Test convert bool value",
+			input: map[string]interface{}{
+				"active": true,
+			},
+			expect: clause.Values{
+				Columns: []clause.Column{{Name: "active"}},
+				Values:  [][]interface{}{{true}},
+			},
+		},
+	}
+
+	for _, tc := range testCase {
+		t.Run(tc.name, func(t *testing.T) {
+			actual := ConvertMapToValuesForCreate(&gorm.Statement{}, tc.input)
+			if !reflect.DeepEqual(actual, tc.expect) {
+				t.Errorf("expect %v got %v", tc.expect, actual)
+			}
+		})
+	}
+}
+
+func TestConvertSliceOfMapToValuesForCreate(t *testing.T) {
+	testCase := []struct {
+		name   string
+		input  []map[string]interface{}
+		expect clause.Values
+	}{
+		{
+			name: "Test convert slice of string value",
+			input: []map[string]interface{}{
+				{"name": "my name"},
+			},
+			expect: clause.Values{
+				Columns: []clause.Column{{Name: "name"}},
+				Values:  [][]interface{}{{"my name"}},
+			},
+		},
+		{
+			name: "Test convert slice of int value",
+			input: []map[string]interface{}{
+				{"age": 18},
+			},
+			expect: clause.Values{
+				Columns: []clause.Column{{Name: "age"}},
+				Values:  [][]interface{}{{18}},
+			},
+		},
+		{
+			name: "Test convert slice of float value",
+			input: []map[string]interface{}{
+				{"score": 99.5},
+			},
+			expect: clause.Values{
+				Columns: []clause.Column{{Name: "score"}},
+				Values:  [][]interface{}{{99.5}},
+			},
+		},
+		{
+			name: "Test convert slice of bool value",
+			input: []map[string]interface{}{
+				{"active": true},
+			},
+			expect: clause.Values{
+				Columns: []clause.Column{{Name: "active"}},
+				Values:  [][]interface{}{{true}},
+			},
+		},
+	}
+
+	for _, tc := range testCase {
+		t.Run(tc.name, func(t *testing.T) {
+			actual := ConvertSliceOfMapToValuesForCreate(&gorm.Statement{}, tc.input)
+
+			if !reflect.DeepEqual(actual, tc.expect) {
+				t.Errorf("expected %v but got %v", tc.expect, actual)
+			}
+		})
+	}
+
+}
diff --git a/callbacks/visit_map_test.go b/callbacks/visit_map_test.go
deleted file mode 100644
index b1fb86d..0000000
--- a/callbacks/visit_map_test.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package callbacks
-
-import (
-	"reflect"
-	"testing"
-)
-
-func TestLoadOrStoreVisitMap(t *testing.T) {
-	var vm visitMap
-	var loaded bool
-	type testM struct {
-		Name string
-	}
-
-	t1 := testM{Name: "t1"}
-	t2 := testM{Name: "t2"}
-	t3 := testM{Name: "t3"}
-
-	vm = make(visitMap)
-	if loaded = loadOrStoreVisitMap(&vm, reflect.ValueOf(&t1)); loaded {
-		t.Fatalf("loaded should be false")
-	}
-
-	if loaded = loadOrStoreVisitMap(&vm, reflect.ValueOf(&t1)); !loaded {
-		t.Fatalf("loaded should be true")
-	}
-
-	// t1 already exist but t2 not
-	if loaded = loadOrStoreVisitMap(&vm, reflect.ValueOf([]*testM{&t1, &t2, &t3})); loaded {
-		t.Fatalf("loaded should be false")
-	}
-
-	if loaded = loadOrStoreVisitMap(&vm, reflect.ValueOf([]*testM{&t2, &t3})); !loaded {
-		t.Fatalf("loaded should be true")
-	}
-}
diff --git a/clause/where_test.go b/clause/where_test.go
index aa9d06e..7d5aca1 100644
--- a/clause/where_test.go
+++ b/clause/where_test.go
@@ -105,6 +105,14 @@ func TestWhere(t *testing.T) {
 			"SELECT * FROM `users` WHERE (`users`.`id` <> ? AND NOT `score` <= ?)",
 			[]interface{}{"1", 100},
 		},
+		{
+			[]clause.Interface{clause.Select{}, clause.From{}, clause.Where{
+				Exprs: []clause.Expression{clause.Not(clause.Expr{SQL: "`score` <= ?", Vars: []interface{}{100}},
+					clause.Expr{SQL: "`age` <= ?", Vars: []interface{}{60}})},
+			}},
+			"SELECT * FROM `users` WHERE NOT (`score` <= ? AND `age` <= ?)",
+			[]interface{}{100, 60},
+		},
 	}
 
 	for idx, result := range results {
diff --git a/tests/query_test.go b/tests/query_test.go
index cadf716..e780e3b 100644
--- a/tests/query_test.go
+++ b/tests/query_test.go
@@ -554,6 +554,11 @@ func TestNot(t *testing.T) {
 	if !regexp.MustCompile("SELECT \\* FROM .*users.* WHERE .*users.*..*name.* <> .+ AND .*users.*..*age.* <> .+").MatchString(result.Statement.SQL.String()) {
 		t.Fatalf("Build NOT condition, but got %v", result.Statement.SQL.String())
 	}
+
+	result = dryDB.Not(DB.Where("manager IS NULL").Where("age >= ?", 20)).Find(&User{})
+	if !regexp.MustCompile("SELECT \\* FROM .*users.* WHERE NOT \\(manager IS NULL AND age >= .+\\) AND .users.\\..deleted_at. IS NULL").MatchString(result.Statement.SQL.String()) {
+		t.Fatalf("Build NOT condition, but got %v", result.Statement.SQL.String())
+	}
 }
 
 func TestNotWithAllFields(t *testing.T) {