feat: support null conditions in custom queries

This commit is contained in:
zhuyasen
2025-01-08 19:44:48 +08:00
parent 090586915a
commit e7dcb143dd
12 changed files with 184 additions and 54 deletions

View File

@@ -97,7 +97,7 @@ type Column struct {
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name"` // column name
Exp string `protobuf:"bytes,2,opt,name=exp,proto3" json:"exp"` // expressions, default value is "=", support =, !=, >, >=, <, <=, like, in
Exp string `protobuf:"bytes,2,opt,name=exp,proto3" json:"exp"` // expressions, default value is "=", support =, !=, >, >=, <, <=, like, in, notin
Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value"` // column value
Logic string `protobuf:"bytes,4,opt,name=logic,proto3" json:"logic"` // logical type, default value is "and", support &, and, ||, or
}

View File

@@ -13,7 +13,7 @@ message Params {
message Column {
string name = 1; // column name
string exp = 2; // expressions, default value is "=", support =, !=, >, >=, <, <=, like, in
string exp = 2; // expressions, default value is "=", support =, !=, >, >=, <, <=, like, in, notin
string value = 3; // column value
string logic = 4; // logical type, default value is "and", support &, and, ||, or
}

View File

@@ -188,7 +188,7 @@ func (d *userExampleDao) GetByID(ctx context.Context, id uint64) (*model.UserExa
// query parameters (not required):
//
// name: column name
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in, notin, isnull, isnotnull
// value: column value, if exp=in, multiple values are separated by commas
// logic: logical type, default value is "and", support &, and, ||, or
//

View File

@@ -194,7 +194,7 @@ func (d *userExampleDao) GetByID(ctx context.Context, id uint64) (*model.UserExa
// query parameters (not required):
//
// name: column name
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in, notin, isnull, isnotnull
// value: column value, if exp=in, multiple values are separated by commas
// logic: logical type, default value is "and", support &, and, ||, or
//
@@ -260,7 +260,7 @@ func (d *userExampleDao) DeleteByIDs(ctx context.Context, ids []uint64) error {
// query conditions:
//
// name: column name
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in, notin, isnull, isnotnull
// value: column value, if exp=in, multiple values are separated by commas
// logic: logical type, default value is "and", support &, and, ||, or
//

View File

@@ -200,7 +200,7 @@ func (d *{{.TableNameCamelFCL}}Dao) GetBy{{.ColumnNameCamel}}(ctx context.Contex
// query parameters (not required):
//
// name: column name
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in, notin, isnull, isnotnull
// value: column value, if exp=in, multiple values are separated by commas
// logic: logical type, default value is "and", support &, and, ||, or
//
@@ -269,7 +269,7 @@ func (d *{{.TableNameCamelFCL}}Dao) DeleteBy{{.ColumnNamePluralCamel}}(ctx conte
// query conditions:
//
// name: column name
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in, notin, isnull, isnotnull
// value: column value, if exp=in, multiple values are separated by commas
// logic: logical type, default value is "and", support &, and, ||, or
//

View File

@@ -206,7 +206,7 @@ func (d *userExampleDao) GetByID(ctx context.Context, id string) (*model.UserExa
// query parameters (not required):
//
// name: column name, if value is of type objectId, the suffix :oid must be added, e.g. order_id:oid
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in, notin
// value: column value, if exp=in, multiple values are separated by commas
// logic: logical type, default value is "and", support &, and, ||, or
//

View File

@@ -211,7 +211,7 @@ func (d *userExampleDao) GetByID(ctx context.Context, id string) (*model.UserExa
// query parameters (not required):
//
// name: column name, if value is of type objectId, the suffix :oid must be added, e.g. order_id:oid
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in, notin
// value: column value, if exp=in, multiple values are separated by commas
// logic: logical type, default value is "and", support &, and, ||, or
//
@@ -284,7 +284,7 @@ func (d *userExampleDao) DeleteByIDs(ctx context.Context, ids []string) error {
// query conditions:
//
// name: column name, if value is of type objectId, the suffix :oid must be added, e.g. post_id:oid
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in, notin
// value: column value, if exp=in, multiple values are separated by commas
// logic: logical type, default value is "and", support &, and, ||, or
//

View File

@@ -194,7 +194,7 @@ func (d *{{.TableNameCamelFCL}}Dao) GetBy{{.ColumnNameCamel}}(ctx context.Contex
// query parameters (not required):
//
// name: column name
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in
// exp: expressions, which default is "=", support =, !=, >, >=, <, <=, like, in, notin, isnull, isnotnull
// value: column value, if exp=in, multiple values are separated by commas
// logic: logical type, default value is "and", support &, and, ||, or
//

View File

@@ -13,6 +13,7 @@
// case 1: specify options in dsn
db, err := mgo.Init("mongodb://root:123456@192.168.3.37:27017/account?socketTimeoutMS=30000&maxPoolSize=100&minPoolSize=1&maxConnIdleTimeMS=300000")
// case 2: specify options in code
db, err := mgo.Init("mongodb://root:123456@192.168.3.37:27017/account",
mgo.WithOption().SetMaxPoolSize(100),

View File

@@ -33,6 +33,8 @@ const (
Like = "like"
// In include
In = "in"
// NotIn exclude
NotIn = "nin"
// AND logic and
AND string = "and" //nolint
@@ -62,6 +64,9 @@ var expMap = map[string]string{
lteSymbol: lteSymbol,
Like: Like,
In: In,
NotIn: NotIn,
"notin": NotIn,
"not in": NotIn,
}
var logicMap = map[string]string{
@@ -152,9 +157,9 @@ func (c *Column) convert() error {
case Like:
escapedValue := regexp.QuoteMeta(fmt.Sprintf("%v", c.Value))
c.Value = bson.M{"$regex": escapedValue, "$options": "i"}
case In:
val, ok := c.Value.(string)
if !ok {
case In, NotIn:
val, ok2 := c.Value.(string)
if !ok2 {
return fmt.Errorf("invalid value type '%s'", c.Value)
}
values := []interface{}{}
@@ -162,10 +167,10 @@ func (c *Column) convert() error {
for _, s := range ss {
values = append(values, s)
}
c.Value = bson.M{"$in": values}
c.Value = bson.M{"$" + c.Exp: values}
}
} else {
return fmt.Errorf("unknown exp type '%s'", c.Exp)
return fmt.Errorf("unsported exp type '%s'", c.Exp)
}
return c.convertLogic()

View File

@@ -23,6 +23,12 @@ const (
Like = "like"
// In include
In = "in"
// NotIN not include
NotIN = "notin"
// IsNull is null
IsNull = "isnull"
// IsNotNull is not null
IsNotNull = "isnotnull"
// AND logic and
AND string = "and"
@@ -31,21 +37,27 @@ const (
)
var expMap = map[string]string{
Eq: " = ",
Neq: " <> ",
Gt: " > ",
Gte: " >= ",
Lt: " < ",
Lte: " <= ",
Like: " LIKE ",
In: " IN ",
Eq: " = ",
Neq: " <> ",
Gt: " > ",
Gte: " >= ",
Lt: " < ",
Lte: " <= ",
Like: " LIKE ",
In: " IN ",
NotIN: " NOT IN ",
IsNull: " IS NULL ",
IsNotNull: " IS NOT NULL ",
"=": " = ",
"!=": " <> ",
">": " > ",
">=": " >= ",
"<": " < ",
"<=": " <= ",
"=": " = ",
"!=": " <> ",
">": " > ",
">=": " >= ",
"<": " < ",
"<=": " <= ",
"not in": " NOT IN ",
"is null": " IS NULL ",
"is not null": " IS NOT NULL ",
}
var logicMap = map[string]string{
@@ -75,7 +87,7 @@ type Params struct {
// Column query info
type Column struct {
Name string `json:"name" form:"name"` // column name
Exp string `json:"exp" form:"exp"` // expressions, default value is "=", support =, !=, >, >=, <, <=, like, in
Exp string `json:"exp" form:"exp"` // expressions, default value is "=", support =, !=, >, >=, <, <=, like, in, notin, isnull, isnotnull
Value interface{} `json:"value" form:"value"` // column value
Logic string `json:"logic" form:"logic"` // logical type, defaults to and when the value is null, with &(and), ||(or)
}
@@ -85,25 +97,48 @@ func (c *Column) checkValid() error {
return fmt.Errorf("field 'name' cannot be empty")
}
if c.Value == nil {
v := expMap[strings.ToLower(c.Exp)]
if v == " IS NULL " || v == " IS NOT NULL " {
return nil
}
return fmt.Errorf("field 'value' cannot be nil")
}
return nil
}
// converting ExpType to sql expressions and LogicType to sql using characters
func (c *Column) convert() error {
func (c *Column) convert() (string, error) {
symbol := "?"
if c.Exp == "" {
c.Exp = Eq
}
if v, ok := expMap[strings.ToLower(c.Exp)]; ok { //nolint
c.Exp = v
if c.Exp == " LIKE " {
c.Value = fmt.Sprintf("%%%v%%", c.Value)
}
if c.Exp == " IN " {
val, ok := c.Value.(string)
if !ok {
return fmt.Errorf("invalid value type '%s'", c.Value)
switch c.Exp {
case " LIKE ":
val, ok1 := c.Value.(string)
if !ok1 {
return symbol, fmt.Errorf("invalid value type '%s'", c.Value)
}
l := len(val)
if l > 2 {
val2 := val[1 : l-1]
val2 = strings.ReplaceAll(val2, "%", "\\%")
val2 = strings.ReplaceAll(val2, "_", "\\_")
val = string(val[0]) + val2 + string(val[l-1])
}
if strings.HasPrefix(val, "%") ||
strings.HasPrefix(val, "_") ||
strings.HasSuffix(val, "%") ||
strings.HasSuffix(val, "_") {
c.Value = val
} else {
c.Value = "%" + val + "%"
}
case " IN ", " NOT IN ":
val, ok1 := c.Value.(string)
if !ok1 {
return symbol, fmt.Errorf("invalid value type '%s'", c.Value)
}
iVal := []interface{}{}
ss := strings.Split(val, ",")
@@ -111,9 +146,13 @@ func (c *Column) convert() error {
iVal = append(iVal, s)
}
c.Value = iVal
symbol = "(?)"
case " IS NULL ", " IS NOT NULL ":
c.Value = nil
symbol = ""
}
} else {
return fmt.Errorf("unknown exp type '%s'", c.Exp)
return symbol, fmt.Errorf("unsported exp type '%s'", c.Exp)
}
if c.Logic == "" {
@@ -122,10 +161,10 @@ func (c *Column) convert() error {
if v, ok := logicMap[strings.ToLower(c.Logic)]; ok { //nolint
c.Logic = v
} else {
return fmt.Errorf("unknown logic type '%s'", c.Logic)
return symbol, fmt.Errorf("unknown logic type '%s'", c.Logic)
}
return nil
return symbol, nil
}
// ConvertToPage converted to page
@@ -158,22 +197,19 @@ func (p *Params) ConvertToGormConditions() (string, []interface{}, error) {
return "", nil, err
}
err := column.convert()
symbol, err := column.convert()
if err != nil {
return "", nil, err
}
symbol := "?"
if column.Exp == " IN " {
symbol = "(?)"
}
if i == l-1 { // ignore the logical type of the last column
str += column.Name + column.Exp + symbol
} else {
str += column.Name + column.Exp + symbol + column.Logic
}
args = append(args, column.Value)
if column.Value != nil {
args = append(args, column.Value)
}
// when multiple columns are the same, determine whether the use of IN
if isUseIN {
if field != column.Name {

View File

@@ -141,13 +141,58 @@ func TestParams_ConvertToGormConditions(t *testing.T) {
columns: []Column{
{
Name: "name",
Value: "Li",
Value: "foo",
Exp: Like,
},
},
},
want: "name LIKE ?",
want1: []interface{}{"%Li%"},
want1: []interface{}{"%foo%"},
wantErr: false,
},
{
name: "1 column like prefix",
args: args{
columns: []Column{
{
Name: "name",
Value: "%foo",
Exp: Like,
},
},
},
want: "name LIKE ?",
want1: []interface{}{"%foo"},
wantErr: false,
},
{
name: "1 column like suffix",
args: args{
columns: []Column{
{
Name: "name",
Value: "foo%",
Exp: Like,
},
},
},
want: "name LIKE ?",
want1: []interface{}{"foo%"},
wantErr: false,
},
{
name: "1 column like with %",
args: args{
columns: []Column{
{
Name: "name",
Value: "f%o_o",
Exp: Like,
},
},
},
want: "name LIKE ?",
want1: []interface{}{"%f\\%o\\_o%"},
wantErr: false,
},
{
@@ -165,6 +210,49 @@ func TestParams_ConvertToGormConditions(t *testing.T) {
want1: []interface{}{[]interface{}{"ab", "cd", "ef"}},
wantErr: false,
},
{
name: "1 column NOT IN",
args: args{
columns: []Column{
{
Name: "name",
Value: "ab,cd,ef",
Exp: NotIN,
},
},
},
want: "name NOT IN (?)",
want1: []interface{}{[]interface{}{"ab", "cd", "ef"}},
wantErr: false,
},
{
name: "1 column IS NULL",
args: args{
columns: []Column{
{
Name: "name",
Exp: IsNull,
},
},
},
want: "name IS NULL ",
want1: []interface{}{},
wantErr: false,
},
{
name: "1 column IS NOT NULL",
args: args{
columns: []Column{
{
Name: "name",
Exp: IsNotNull,
},
},
},
want: "name IS NOT NULL ",
want1: []interface{}{},
wantErr: false,
},
// --------------------------- query 2 columns ------------------------------
{
@@ -444,14 +532,14 @@ func TestParams_ConvertToGormConditions(t *testing.T) {
}
got, got1, err := params.ConvertToGormConditions()
if (err != nil) != tt.wantErr {
t.Errorf("ConvertToGormConditions() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("ConvertToGormConditions() error = %v, wantErr = %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ConvertToGormConditions() got = %v, want %v", got, tt.want)
t.Errorf("ConvertToGormConditions() got = %v, want = %v", got, tt.want)
}
if !reflect.DeepEqual(got1, tt.want1) {
t.Errorf("ConvertToGormConditions() got1 = %v, want %v", got1, tt.want1)
t.Errorf("ConvertToGormConditions() got1 = %v, want = %v", got1, tt.want1)
}
got = strings.Replace(got, "?", "%v", -1)