diff --git a/contrib/drivers/mysql/mysql_z_unit_issue_test.go b/contrib/drivers/mysql/mysql_z_unit_issue_test.go index 58ceeb0a5f3..549988036d4 100644 --- a/contrib/drivers/mysql/mysql_z_unit_issue_test.go +++ b/contrib/drivers/mysql/mysql_z_unit_issue_test.go @@ -1409,3 +1409,51 @@ func Test_Issue3968(t *testing.T) { t.Assert(len(result), 10) }) } + +// https://github.com/gogf/gf/issues/3915 +func Test_Issue3915(t *testing.T) { + table := "issue3915" + array := gstr.SplitAndTrim(gtest.DataContent(`issue3915.sql`), ";") + for _, v := range array { + if _, err := db.Exec(ctx, v); err != nil { + gtest.Error(err) + } + } + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + //db.SetDebug(true) + all, err := db.Model(table).Where("a < b").All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 1) + + all, err = db.Model(table).Where(gdb.Raw("a < b")).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 1) + + all, err = db.Model(table).WhereLT("a", gdb.Raw("`b`")).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 1) + }) + + gtest.C(t, func(t *gtest.T) { + //db.SetDebug(true) + all, err := db.Model(table).Where("a > b").All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 2) + + all, err = db.Model(table).Where(gdb.Raw("a > b")).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 2) + + all, err = db.Model(table).WhereGT("a", gdb.Raw("`b`")).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 2) + }) +} diff --git a/contrib/drivers/mysql/testdata/issue3915.sql b/contrib/drivers/mysql/testdata/issue3915.sql new file mode 100644 index 00000000000..6fa6b86c8df --- /dev/null +++ b/contrib/drivers/mysql/testdata/issue3915.sql @@ -0,0 +1,9 @@ +CREATE TABLE `issue3915` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'user id', + `a` float DEFAULT NULL COMMENT 'user name', + `b` float DEFAULT NULL COMMENT 'user status', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO `issue3915` (`id`,`a`,`b`) VALUES (1,1,2); +INSERT INTO `issue3915` (`id`,`a`,`b`) VALUES (2,5,4); \ No newline at end of file diff --git a/database/gdb/gdb_core.go b/database/gdb/gdb_core.go index ec14ffd1be2..0a88a17ecc6 100644 --- a/database/gdb/gdb_core.go +++ b/database/gdb/gdb_core.go @@ -789,9 +789,5 @@ func (c *Core) IsSoftCreatedFieldName(fieldName string) bool { // The internal handleArguments function might be called twice during the SQL procedure, // but do not worry about it, it's safe and efficient. func (c *Core) FormatSqlBeforeExecuting(sql string, args []interface{}) (newSql string, newArgs []interface{}) { - // DO NOT do this as there may be multiple lines and comments in the sql. - // sql = gstr.Trim(sql) - // sql = gstr.Replace(sql, "\n", " ") - // sql, _ = gregex.ReplaceString(`\s{2,}`, ` `, sql) return handleSliceAndStructArgsForSql(sql, args) } diff --git a/database/gdb/gdb_func.go b/database/gdb/gdb_func.go index 752710d1cf4..ce5258e9983 100644 --- a/database/gdb/gdb_func.go +++ b/database/gdb/gdb_func.go @@ -608,7 +608,7 @@ func formatWhereHolder(ctx context.Context, db DB, in formatWhereHolderInput) (n // =============================================================== if subModel, ok := in.Args[i].(*Model); ok { index := -1 - whereStr, _ = gregex.ReplaceStringFunc(`(\?)`, whereStr, func(s string) string { + whereStr = gstr.ReplaceFunc(whereStr, `?`, func(s string) string { index++ if i+len(newArgs) == index { sqlWithHolder, holderArgs := subModel.getHolderAndArgsAsSubModel(ctx) @@ -843,7 +843,7 @@ func handleSliceAndStructArgsForSql( counter = 0 replaced = false ) - newSql, _ = gregex.ReplaceStringFunc(`\?`, newSql, func(s string) string { + newSql = gstr.ReplaceFunc(newSql, `?`, func(s string) string { if replaced { return s } @@ -856,9 +856,20 @@ func handleSliceAndStructArgsForSql( return s }) - // Special struct handling. - case reflect.Struct: + default: switch oldArg.(type) { + // Do not append Raw arg to args but directly into the sql. + case Raw, *Raw: + var counter = 0 + newSql = gstr.ReplaceFunc(newSql, `?`, func(s string) string { + counter++ + if counter == index+insertHolderCount+1 { + return gconv.String(oldArg) + } + return s + }) + continue + // The underlying driver supports time.Time/*time.Time types. case time.Time, *time.Time: newArgs = append(newArgs, oldArg) @@ -881,9 +892,6 @@ func handleSliceAndStructArgsForSql( } } newArgs = append(newArgs, oldArg) - - default: - newArgs = append(newArgs, oldArg) } } return diff --git a/text/gstr/gstr_replace.go b/text/gstr/gstr_replace.go index 1449f9be005..94b650813ef 100644 --- a/text/gstr/gstr_replace.go +++ b/text/gstr/gstr_replace.go @@ -92,3 +92,96 @@ func ReplaceIByMap(origin string, replaces map[string]string) string { } return origin } + +// ReplaceFunc returns a copy of the string `origin` in which each non-overlapping substring +// that matches the given search string is replaced by the result of function `f` applied to that substring. +// The function `f` is called with each matching substring as its argument and must return a string to be used +// as the replacement value. +func ReplaceFunc(origin string, search string, f func(string) string) string { + if search == "" { + return origin + } + var ( + searchLen = len(search) + originLen = len(origin) + ) + // If search string is longer than origin string, no match is possible + if searchLen > originLen { + return origin + } + var ( + result strings.Builder + lastMatch int + currentPos int + ) + // Pre-allocate the builder capacity to avoid reallocations + result.Grow(originLen) + + for currentPos < originLen { + pos := Pos(origin[currentPos:], search) + if pos == -1 { + break + } + pos += currentPos + // Append unmatched portion + result.WriteString(origin[lastMatch:pos]) + // Apply replacement function and append result + match := origin[pos : pos+searchLen] + result.WriteString(f(match)) + // Update positions + lastMatch = pos + searchLen + currentPos = lastMatch + } + // Append remaining unmatched portion + if lastMatch < originLen { + result.WriteString(origin[lastMatch:]) + } + return result.String() +} + +// ReplaceIFunc returns a copy of the string `origin` in which each non-overlapping substring +// that matches the given search string is replaced by the result of function `f` applied to that substring. +// The match is done case-insensitively. +// The function `f` is called with each matching substring as its argument and must return a string to be used +// as the replacement value. +func ReplaceIFunc(origin string, search string, f func(string) string) string { + if search == "" { + return origin + } + var ( + searchLen = len(search) + originLen = len(origin) + ) + // If search string is longer than origin string, no match is possible + if searchLen > originLen { + return origin + } + var ( + result strings.Builder + lastMatch int + currentPos int + ) + // Pre-allocate the builder capacity to avoid reallocations + result.Grow(originLen) + + for currentPos < originLen { + pos := PosI(origin[currentPos:], search) + if pos == -1 { + break + } + pos += currentPos + // Append unmatched portion + result.WriteString(origin[lastMatch:pos]) + // Apply replacement function and append result + match := origin[pos : pos+searchLen] + result.WriteString(f(match)) + // Update positions + lastMatch = pos + searchLen + currentPos = lastMatch + } + // Append remaining unmatched portion + if lastMatch < originLen { + result.WriteString(origin[lastMatch:]) + } + return result.String() +} diff --git a/text/gstr/gstr_z_example_test.go b/text/gstr/gstr_z_example_test.go index c3499dda54b..afaefee0b61 100644 --- a/text/gstr/gstr_z_example_test.go +++ b/text/gstr/gstr_z_example_test.go @@ -8,6 +8,8 @@ package gstr_test import ( "fmt" + "strconv" + "strings" "github.com/gogf/gf/v2/text/gstr" ) @@ -1018,6 +1020,51 @@ func ExampleReplaceIByMap() { // goframe is very nice } +func ExampleReplaceFunc() { + str := "hello gf 2018~2020!" + // Replace "gf" with a custom function that returns "GoFrame" + result := gstr.ReplaceFunc(str, "gf", func(s string) string { + return "GoFrame" + }) + fmt.Println(result) + + // Replace numbers with their doubled values + result = gstr.ReplaceFunc("1 2 3", "2", func(s string) string { + n, _ := strconv.Atoi(s) + return strconv.Itoa(n * 2) + }) + fmt.Println(result) + + // Output: + // hello GoFrame 2018~2020! + // 1 4 3 +} + +func ExampleReplaceIFunc() { + str := "Hello GF, hello gf, HELLO Gf!" + // Replace any case variation of "gf" with "GoFrame" + result := gstr.ReplaceIFunc(str, "gf", func(s string) string { + return "GoFrame" + }) + fmt.Println(result) + + // Preserve the original case of each match + result = gstr.ReplaceIFunc(str, "gf", func(s string) string { + if s == strings.ToUpper(s) { + return "GOFRAME" + } + if s == strings.ToLower(s) { + return "goframe" + } + return "GoFrame" + }) + fmt.Println(result) + + // Output: + // Hello GoFrame, hello GoFrame, HELLO GoFrame! + // Hello GOFRAME, hello goframe, HELLO GoFrame! +} + // similartext func ExampleSimilarText() { var ( diff --git a/text/gstr/gstr_z_unit_replace_test.go b/text/gstr/gstr_z_unit_replace_test.go index 81e049f3745..fbc66fca61c 100644 --- a/text/gstr/gstr_z_unit_replace_test.go +++ b/text/gstr/gstr_z_unit_replace_test.go @@ -9,6 +9,7 @@ package gstr_test import ( + "strings" "testing" "github.com/gogf/gf/v2/frame/g" @@ -88,3 +89,143 @@ func Test_ReplaceI_2(t *testing.T) { t.Assert(gstr.ReplaceI("aaa", "A", "AA", 4), `AAAAAA`) }) } + +func Test_ReplaceIFunc(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + origin = "hello GF 2018~2020!" + search = "gf" + ) + // Simple replacement + result := gstr.ReplaceIFunc(origin, search, func(s string) string { + return "GoFrame" + }) + t.Assert(result, "hello GoFrame 2018~2020!") + + // Replace with original string + result = gstr.ReplaceIFunc(origin, search, func(s string) string { + return s + }) + t.Assert(result, origin) + + // Replace with empty string + result = gstr.ReplaceIFunc(origin, search, func(s string) string { + return "" + }) + t.Assert(result, "hello 2018~2020!") + + // Replace multiple occurrences with different cases + origin = "GF is best, gf is nice, Gf is excellent" + result = gstr.ReplaceIFunc(origin, search, func(s string) string { + return "GoFrame" + }) + t.Assert(result, "GoFrame is best, GoFrame is nice, GoFrame is excellent") + + // Empty search string + result = gstr.ReplaceIFunc(origin, "", func(s string) string { + return "GoFrame" + }) + t.Assert(result, origin) + + // Empty origin string + result = gstr.ReplaceIFunc("", search, func(s string) string { + return "GoFrame" + }) + t.Assert(result, "") + + // Replace with longer string + result = gstr.ReplaceIFunc("GF", search, func(s string) string { + return "GoFrame" + }) + t.Assert(result, "GoFrame") + + // Replace with shorter string + result = gstr.ReplaceIFunc("GF", search, func(s string) string { + return "g" + }) + t.Assert(result, "g") + + // Replace with mixed case patterns + origin = "gf GF Gf gF" + result = gstr.ReplaceIFunc(origin, search, func(s string) string { + return strings.ToUpper(s) + }) + t.Assert(result, "GF GF GF GF") + }) +} + +func Test_ReplaceFunc(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + origin = "hello gf 2018~2020!" + search = "gf" + ) + // Simple replacement + result := gstr.ReplaceFunc(origin, search, func(s string) string { + return "GoFrame" + }) + t.Assert(result, "hello GoFrame 2018~2020!") + + // Replace with original string + result = gstr.ReplaceFunc(origin, search, func(s string) string { + return s + }) + t.Assert(result, origin) + + // Replace with empty string + result = gstr.ReplaceFunc(origin, search, func(s string) string { + return "" + }) + t.Assert(result, "hello 2018~2020!") + + // Replace multiple occurrences + origin = "gf is best, gf is nice" + result = gstr.ReplaceFunc(origin, search, func(s string) string { + return "GoFrame" + }) + t.Assert(result, "GoFrame is best, GoFrame is nice") + + // Empty search string + result = gstr.ReplaceFunc(origin, "", func(s string) string { + return "GoFrame" + }) + t.Assert(result, origin) + + // Empty origin string + result = gstr.ReplaceFunc("", search, func(s string) string { + return "GoFrame" + }) + t.Assert(result, "") + + // Case sensitive + origin = "GF is best, gf is nice" + result = gstr.ReplaceFunc(origin, search, func(s string) string { + return "GoFrame" + }) + t.Assert(result, "GF is best, GoFrame is nice") + + // Replace with longer string + result = gstr.ReplaceFunc("gf", search, func(s string) string { + return "GoFrame" + }) + t.Assert(result, "GoFrame") + + // Replace with shorter string + result = gstr.ReplaceFunc("gf", search, func(s string) string { + return "g" + }) + t.Assert(result, "g") + }) + gtest.C(t, func(t *gtest.T) { + var ( + origin = "gggg" + search = "g" + replace = "gg" + ) + // Simple replacement + result := gstr.ReplaceFunc(origin, search, func(s string) string { + return replace + }) + t.Assert(result, "gggggggg") + }) +}