diff --git a/contrib/drivers/mysql/mysql_z_unit_model_where_test.go b/contrib/drivers/mysql/mysql_z_unit_model_where_test.go index c0007c6885a..f030cbee593 100644 --- a/contrib/drivers/mysql/mysql_z_unit_model_where_test.go +++ b/contrib/drivers/mysql/mysql_z_unit_model_where_test.go @@ -1053,6 +1053,86 @@ func Test_Model_WherePrefixLike(t *testing.T) { }) } +func Test_Model_WhereOrPrefixLike(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1). + FieldsPrefix(table1, "*"). + LeftJoinOnField(table2, "id"). + WhereOrPrefix(table1, g.Map{ + "id": g.Slice{1, 2}, + }). + WhereOrPrefixLike(table2, "nickname", "name_3%"). + Order("id asc").All() + t.AssertNil(err) + t.Assert(len(r), 3) + t.Assert(r[0]["id"], "1") + t.Assert(r[1]["id"], "2") + t.Assert(r[2]["id"], "3") + }) +} + +func Test_Model_WhereOrPrefixLikeLiteral(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1). + FieldsPrefix(table1, "*"). + LeftJoinOnField(table2, "id"). + WhereOrPrefix(table1, g.Map{ + "id": g.Slice{1, 2}, + }). + WhereOrPrefixLikeLiteral(table2, "nickname", "name_3"). + Order("id asc").All() + t.AssertNil(err) + t.Assert(len(r), 3) + t.Assert(r[0]["id"], "1") + t.Assert(r[1]["id"], "2") + t.Assert(r[2]["id"], "3") + }) +} + +func Test_Model_WhereOrPrefixNotLikeLiteral(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1). + FieldsPrefix(table1, "*"). + LeftJoinOnField(table2, "id"). + WhereOrPrefix(table1, g.Map{ + "id": g.Slice{1, 2}, + }). + WhereOrPrefixNotLikeLiteral(table2, "nickname", "name_1"). + Order("id asc").All() + t.AssertNil(err) + t.Assert(len(r), 10) + t.Assert(r[0]["id"], "1") + t.Assert(r[1]["id"], "2") + }) +} + func Test_Model_WhereExists(t *testing.T) { table := createInitTable() defer dropTable(table) diff --git a/database/gdb/gdb_func.go b/database/gdb/gdb_func.go index 437c981f8fe..b58983019a5 100644 --- a/database/gdb/gdb_func.go +++ b/database/gdb/gdb_func.go @@ -1015,3 +1015,40 @@ func genTableNamesCacheKey(group string) string { func genSoftTimeFieldNameTypeCacheKey(schema, table string, candidateFields []string) string { return fmt.Sprintf(`getSoftFieldNameAndType:%s#%s#%s`, schema, table, strings.Join(candidateFields, "_")) } + +// EscapeLikeString escapes special characters in a string for LIKE operations. +// It escapes '\', '%', and '_' characters to prevent them from being interpreted +// as wildcard characters in SQL LIKE statements. +// +// This function is useful when you need to search for literal characters that would +// otherwise be treated as wildcards in LIKE patterns. Use this when accepting user +// input for LIKE operations to prevent unintended wildcard matching. +// +// The function follows standard SQL escaping rules: +// - '\' becomes '\\' +// - '%' becomes '\%' +// - '_' becomes '\_' +// +// Usage examples: +// +// // Search for exact text containing special characters +// userInput := "user%name_test" +// escaped := gdb.EscapeLikeString(userInput) // "user\\%name\\_test" +// db.Model("users").WhereLike("username", escaped) +// +// // Search for text containing special characters with wildcards +// userInput := "user%name" +// escaped := gdb.EscapeLikeString(userInput) // "user\\%name" +// db.Model("users").WhereLike("username", "%"+escaped+"%") // LIKE '%user\%name%' +// +// // Normal wildcard usage (do NOT escape) +// db.Model("users").WhereLike("username", "user%") // LIKE 'user%' - matches userABC, user123, etc. +func escapeLikeString(s string) string { + // Escape backslashes first to prevent double escaping + s = strings.ReplaceAll(s, "\\", "\\\\") + // Escape percent signs + s = strings.ReplaceAll(s, "%", "\\%") + // Escape underscores + s = strings.ReplaceAll(s, "_", "\\_") + return s +} diff --git a/database/gdb/gdb_model_builder_where.go b/database/gdb/gdb_model_builder_where.go index 581bdec3bae..a72ca0937f5 100644 --- a/database/gdb/gdb_model_builder_where.go +++ b/database/gdb/gdb_model_builder_where.go @@ -10,6 +10,7 @@ import ( "fmt" "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" ) // doWhereType sets the condition statement for the model. The parameter `where` can be type of @@ -117,6 +118,18 @@ func (b *WhereBuilder) WhereLike(column string, like string) *WhereBuilder { return b.Wheref(`%s LIKE ?`, b.model.QuoteWord(column), like) } +// WhereLikeLiteral builds `column LIKE like` statement with automatic escaping of special characters. +// This method automatically escapes '%', '_', and '\' characters in the like parameter to treat them as literal characters. +// Use this method when you want to search for exact text that may contain LIKE special characters. +// +// Example: +// +// db.Model("user").WhereLikeLiteral("name", "john%doe_123") // Will search for exact string "john%doe_123" +// db.Model("user").WhereLike("name", "john%") // Will search using % as wildcard +func (b *WhereBuilder) WhereLikeLiteral(column string, like string) *WhereBuilder { + return b.Wheref(`%s LIKE ?`, b.model.QuoteWord(column), escapeLikeString(like)) +} + // WhereIn builds `column IN (in)` statement. func (b *WhereBuilder) WhereIn(column string, in any) *WhereBuilder { return b.doWherefType(whereHolderTypeIn, `%s IN (?)`, b.model.QuoteWord(column), in) @@ -141,6 +154,12 @@ func (b *WhereBuilder) WhereNotLike(column string, like any) *WhereBuilder { return b.Wheref(`%s NOT LIKE ?`, b.model.QuoteWord(column), like) } +// WhereNotLikeLiteral builds `column NOT LIKE like` statement with automatic escaping of special characters. +// This method automatically escapes '%', '_', and '\' characters in the like parameter to treat them as literal characters. +func (b *WhereBuilder) WhereNotLikeLiteral(column string, like any) *WhereBuilder { + return b.Wheref(`%s NOT LIKE ?`, b.model.QuoteWord(column), escapeLikeString(gconv.String(like))) +} + // WhereNot builds `column != value` statement. func (b *WhereBuilder) WhereNot(column string, value any) *WhereBuilder { return b.Wheref(`%s != ?`, b.model.QuoteWord(column), value) diff --git a/database/gdb/gdb_model_builder_where_prefix.go b/database/gdb/gdb_model_builder_where_prefix.go index 67ebc254c52..a66f9a65ff9 100644 --- a/database/gdb/gdb_model_builder_where_prefix.go +++ b/database/gdb/gdb_model_builder_where_prefix.go @@ -6,6 +6,8 @@ package gdb +import "github.com/gogf/gf/v2/util/gconv" + // WherePrefix performs as Where, but it adds prefix to each field in where statement. // Eg: // WherePrefix("order", "status", "paid") => WHERE `order`.`status`='paid' @@ -57,6 +59,12 @@ func (b *WhereBuilder) WherePrefixLike(prefix string, column string, like any) * return b.Wheref(`%s.%s LIKE ?`, b.model.QuoteWord(prefix), b.model.QuoteWord(column), like) } +// WherePrefixLikeLiteral builds `prefix.column LIKE like` statement with automatic escaping. +// This method automatically escapes '%', '_', and '\' characters in the like parameter to treat them as literal characters. +func (b *WhereBuilder) WherePrefixLikeLiteral(prefix string, column string, like any) *WhereBuilder { + return b.Wheref(`%s.%s LIKE ?`, b.model.QuoteWord(prefix), b.model.QuoteWord(column), escapeLikeString(gconv.String(like))) +} + // WherePrefixIn builds `prefix.column IN (in)` statement. func (b *WhereBuilder) WherePrefixIn(prefix string, column string, in any) *WhereBuilder { return b.doWherefType(whereHolderTypeIn, `%s.%s IN (?)`, b.model.QuoteWord(prefix), b.model.QuoteWord(column), in) @@ -81,6 +89,12 @@ func (b *WhereBuilder) WherePrefixNotLike(prefix string, column string, like any return b.Wheref(`%s.%s NOT LIKE ?`, b.model.QuoteWord(prefix), b.model.QuoteWord(column), like) } +// WherePrefixNotLikeLiteral builds `prefix.column NOT LIKE like` statement with automatic escaping. +// This method automatically escapes '%', '_', and '\' characters in the like parameter to treat them as literal characters. +func (b *WhereBuilder) WherePrefixNotLikeLiteral(prefix string, column string, like any) *WhereBuilder { + return b.Wheref(`%s.%s NOT LIKE ?`, b.model.QuoteWord(prefix), b.model.QuoteWord(column), escapeLikeString(gconv.String(like))) +} + // WherePrefixNot builds `prefix.column != value` statement. func (b *WhereBuilder) WherePrefixNot(prefix string, column string, value any) *WhereBuilder { return b.Wheref(`%s.%s != ?`, b.model.QuoteWord(prefix), b.model.QuoteWord(column), value) diff --git a/database/gdb/gdb_model_builder_whereor.go b/database/gdb/gdb_model_builder_whereor.go index 33d32f1382b..f178f8d8dfe 100644 --- a/database/gdb/gdb_model_builder_whereor.go +++ b/database/gdb/gdb_model_builder_whereor.go @@ -10,6 +10,7 @@ import ( "fmt" "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" ) // WhereOr adds "OR" condition to the where statement. @@ -86,6 +87,12 @@ func (b *WhereBuilder) WhereOrLike(column string, like any) *WhereBuilder { return b.WhereOrf(`%s LIKE ?`, b.model.QuoteWord(column), like) } +// WhereOrLikeLiteral builds `column LIKE 'like'` statement in `OR` conditions with automatic escaping. +// This method automatically escapes '%', '_', and '\' characters in the like parameter to treat them as literal characters. +func (b *WhereBuilder) WhereOrLikeLiteral(column string, like any) *WhereBuilder { + return b.WhereOrf(`%s LIKE ?`, b.model.QuoteWord(column), escapeLikeString(gconv.String(like))) +} + // WhereOrIn builds `column IN (in)` statement in `OR` conditions. func (b *WhereBuilder) WhereOrIn(column string, in any) *WhereBuilder { return b.doWhereOrfType(whereHolderTypeIn, `%s IN (?)`, b.model.QuoteWord(column), in) @@ -110,6 +117,12 @@ func (b *WhereBuilder) WhereOrNotLike(column string, like any) *WhereBuilder { return b.WhereOrf(`%s NOT LIKE ?`, b.model.QuoteWord(column), like) } +// WhereOrNotLikeLiteral builds `column NOT LIKE like` statement in `OR` conditions with automatic escaping. +// This method automatically escapes '%', '_', and '\' characters in the like parameter to treat them as literal characters. +func (b *WhereBuilder) WhereOrNotLikeLiteral(column string, like any) *WhereBuilder { + return b.WhereOrf(`%s NOT LIKE ?`, b.model.QuoteWord(column), escapeLikeString(gconv.String(like))) +} + // WhereOrNotIn builds `column NOT IN (in)` statement. func (b *WhereBuilder) WhereOrNotIn(column string, in any) *WhereBuilder { return b.doWhereOrfType(whereHolderTypeIn, `%s NOT IN (?)`, b.model.QuoteWord(column), in) diff --git a/database/gdb/gdb_model_builder_whereor_prefix.go b/database/gdb/gdb_model_builder_whereor_prefix.go index 8707584049b..60908f80a73 100644 --- a/database/gdb/gdb_model_builder_whereor_prefix.go +++ b/database/gdb/gdb_model_builder_whereor_prefix.go @@ -6,6 +6,8 @@ package gdb +import "github.com/gogf/gf/v2/util/gconv" + // WhereOrPrefix performs as WhereOr, but it adds prefix to each field in where statement. // Eg: // WhereOrPrefix("order", "status", "paid") => WHERE xxx OR (`order`.`status`='paid') @@ -59,6 +61,12 @@ func (b *WhereBuilder) WhereOrPrefixLike(prefix string, column string, like any) return b.WhereOrf(`%s.%s LIKE ?`, b.model.QuoteWord(prefix), b.model.QuoteWord(column), like) } +// WhereOrPrefixLikeLiteral builds `prefix.column LIKE 'like'` statement in `OR` conditions with automatic escaping. +// This method automatically escapes '%', '_', and '\' characters in the like parameter to treat them as literal characters. +func (b *WhereBuilder) WhereOrPrefixLikeLiteral(prefix string, column string, like any) *WhereBuilder { + return b.WhereOrf(`%s.%s LIKE ?`, b.model.QuoteWord(prefix), b.model.QuoteWord(column), escapeLikeString(gconv.String(like))) +} + // WhereOrPrefixIn builds `prefix.column IN (in)` statement in `OR` conditions. func (b *WhereBuilder) WhereOrPrefixIn(prefix string, column string, in any) *WhereBuilder { return b.doWhereOrfType(whereHolderTypeIn, `%s.%s IN (?)`, b.model.QuoteWord(prefix), b.model.QuoteWord(column), in) @@ -83,6 +91,12 @@ func (b *WhereBuilder) WhereOrPrefixNotLike(prefix string, column string, like a return b.WhereOrf(`%s.%s NOT LIKE ?`, b.model.QuoteWord(prefix), b.model.QuoteWord(column), like) } +// WhereOrPrefixNotLikeLiteral builds `prefix.column NOT LIKE 'like'` statement in `OR` conditions with automatic escaping. +// This method automatically escapes '%', '_', and '\' characters in the like parameter to treat them as literal characters. +func (b *WhereBuilder) WhereOrPrefixNotLikeLiteral(prefix string, column string, like any) *WhereBuilder { + return b.WhereOrf(`%s.%s NOT LIKE ?`, b.model.QuoteWord(prefix), b.model.QuoteWord(column), escapeLikeString(gconv.String(like))) +} + // WhereOrPrefixNotIn builds `prefix.column NOT IN (in)` statement. func (b *WhereBuilder) WhereOrPrefixNotIn(prefix string, column string, in any) *WhereBuilder { return b.doWhereOrfType(whereHolderTypeIn, `%s.%s NOT IN (?)`, b.model.QuoteWord(prefix), b.model.QuoteWord(column), in) diff --git a/database/gdb/gdb_model_whereor_prefix.go b/database/gdb/gdb_model_whereor_prefix.go index 5b2adb46d76..ea0f7481840 100644 --- a/database/gdb/gdb_model_whereor_prefix.go +++ b/database/gdb/gdb_model_whereor_prefix.go @@ -48,6 +48,12 @@ func (m *Model) WhereOrPrefixLike(prefix string, column string, like any) *Model return m.callWhereBuilder(m.whereBuilder.WhereOrPrefixLike(prefix, column, like)) } +// WhereOrPrefixLikeLiteral builds `prefix.column LIKE 'like'` statement in `OR` conditions with automatic escaping. +// See WhereBuilder.WhereOrPrefixLikeLiteral. +func (m *Model) WhereOrPrefixLikeLiteral(prefix string, column string, like any) *Model { + return m.callWhereBuilder(m.whereBuilder.WhereOrPrefixLikeLiteral(prefix, column, like)) +} + // WhereOrPrefixIn builds `prefix.column IN (in)` statement in `OR` conditions. // See WhereBuilder.WhereOrPrefixIn. func (m *Model) WhereOrPrefixIn(prefix string, column string, in any) *Model { @@ -72,6 +78,12 @@ func (m *Model) WhereOrPrefixNotLike(prefix string, column string, like any) *Mo return m.callWhereBuilder(m.whereBuilder.WhereOrPrefixNotLike(prefix, column, like)) } +// WhereOrPrefixNotLikeLiteral builds `prefix.column NOT LIKE 'like'` statement in `OR` conditions with automatic escaping. +// See WhereBuilder.WhereOrPrefixNotLikeLiteral. +func (m *Model) WhereOrPrefixNotLikeLiteral(prefix string, column string, like any) *Model { + return m.callWhereBuilder(m.whereBuilder.WhereOrPrefixNotLikeLiteral(prefix, column, like)) +} + // WhereOrPrefixNotIn builds `prefix.column NOT IN (in)` statement. // See WhereBuilder.WhereOrPrefixNotIn. func (m *Model) WhereOrPrefixNotIn(prefix string, column string, in any) *Model {