diff --git a/builder/README.md b/builder/README.md index 1c4f537..6af8d5a 100644 --- a/builder/README.md +++ b/builder/README.md @@ -14,7 +14,7 @@ package main import ( "database/sql" _ "github.com/go-sql-driver/mysql" - qb "github.com/didi/gendry/builder" + "github.com/didi/gendry/builder" ) func main() { @@ -22,7 +22,9 @@ func main() { if nil != err { panic(err) } - mp := map[string]interface{}{ + where := map[string]interface{}{ + "_custom_0": builder.Custom("name=?", "name0"), + "_custom_1": builder.JsonContains("my_json->'$.list'", 1), "country": "China", "role": "driver", "age >": 45, @@ -44,10 +46,10 @@ func main() { }, "_orderby": "age desc", } - cond,vals,err := qb.BuildSelect("tableName", where, []string{"name", "count(price) as total", "age"}) + cond,vals,err := builder.BuildSelect("tableName", where, []string{"name", "count(price) as total", "age"}) - //cond: SELECT name,count(price) as total,age FROM tableName WHERE (((x1=? AND x2>=?) OR (x3=? AND x4!=?)) AND country=? AND gmt_create < gmt_modified AND role=? AND age>?) GROUP BY name HAVING (total>? AND total<=?) ORDER BY age DESC - //vals: []interface{}{11, 45, "234", "tx2", "China", "driver", 45, 1000, 50000} + //cond: SELECT name,count(price) as total,age FROM tableName WHERE (name=? AND (? MEMBER OF(my_json->'$.list')) AND ((x1=? AND x2>=?) OR (x3=? AND x4!=?)) AND country=? AND role=? AND age>? AND gmt_create? AND total<=?) ORDER BY age desc + //vals: []interface{}{"name0", 1, 11, 45, "234", "tx2", "China", "driver", 45, 1000, 50000} if nil != err { panic(err) @@ -111,10 +113,12 @@ others supported: * _having * _limit * _lockMode +* _custom_xxx ``` go where := map[string]interface{}{ "age >": 100, + "_custom_1": builder.JsonContains("my_json->'$.list'", 1), "_or": []map[string]interface{}{ { "x1": 11, @@ -140,6 +144,8 @@ Note: * value of _lockMode only supports `share` and `exclusive` temporarily: * `share` representative `SELECT ... LOCK IN SHARE MODE`. Unfortunately, the current version does not support `SELECT ... FOR SHARE`, It'll be supported in the future. * `exclusive` representative `SELECT ... FOR UPDATE` +* if key starts with `_custom_`, the corresponding value must be a `builder.Comparable`. We provide builtin type such as `Custom` and `JsonContains`. You can also provide your own implementation if you want +* `JsonSet`,`JsonArrayAppend`,`JsonArrayInsert`,`JsonRemove` should be used in update map rather than where map #### Aggregate @@ -199,6 +205,7 @@ where := map[string]interface{}{ update := map[string]interface{}{ "role": "primaryschoolstudent", "rank": 5, + "_custom_0": qb.JsonArrayAppend("my_json", "$", 0, "$", 1), } cond,vals,err := qb.BuildUpdate("table_name", where, update) diff --git a/builder/builder.go b/builder/builder.go index 5f25222..98ebd5e 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -22,6 +22,7 @@ var ( errLockModeValueType = errors.New(`[builder] the value of "_lockMode" must be of string type`) errNotAllowedLockMode = errors.New(`[builder] the value of "_lockMode" is not allowed`) errLimitType = errors.New(`[builder] the value of "_limit" must be one of int,uint,int64,uint64`) + errCustomValueType = errors.New(`[builder] the value of "_custom_" must impl Comparable`) errWhereInterfaceSliceType = `[builder] the value of "xxx %s" must be of []interface{} type` errEmptySliceCondition = `[builder] the value of "%s" must contain at least one element` @@ -252,7 +253,15 @@ func getWhereConditions(where map[string]interface{}, ignoreKeys map[string]stru var comparables []Comparable var field, operator string var err error - for key, val := range where { + // to keep the result in certain order + keys := make([]string, 0, len(where)) + for key := range where { + keys = append(keys, key) + } + defaultSortAlgorithm(keys) + + for _, key := range keys { + val := where[key] if _, ok := ignoreKeys[key]; ok { continue } @@ -278,6 +287,14 @@ func getWhereConditions(where map[string]interface{}, ignoreKeys map[string]stru comparables = append(comparables, OrWhere(orWhereComparable)) continue } + if strings.HasPrefix(key, "_custom_") { + v, ok := val.(Comparable) + if !ok { + return nil, errCustomValueType + } + comparables = append(comparables, v) + continue + } field, operator, err = splitKey(key, val) if nil != err { return nil, err diff --git a/builder/dao.go b/builder/dao.go index c2092d6..7c0fb9a 100644 --- a/builder/dao.go +++ b/builder/dao.go @@ -18,13 +18,13 @@ var ( } ) -//the order of a map is unpredicatable so we need a sort algorithm to sort the fields -//and make it predicatable +// the order of a map is unpredicatable so we need a sort algorithm to sort the fields +// and make it predicatable var ( defaultSortAlgorithm = sort.Strings ) -//Comparable requires type implements the Build method +// Comparable requires type implements the Build method type Comparable interface { Build() ([]string, []interface{}) } @@ -123,58 +123,58 @@ func (l NotLike) Build() ([]string, []interface{}) { return cond, vals } -//Eq means equal(=) +// Eq means equal(=) type Eq map[string]interface{} -//Build implements the Comparable interface +// Build implements the Comparable interface func (e Eq) Build() ([]string, []interface{}) { return build(e, "=") } -//Ne means Not Equal(!=) +// Ne means Not Equal(!=) type Ne map[string]interface{} -//Build implements the Comparable interface +// Build implements the Comparable interface func (n Ne) Build() ([]string, []interface{}) { return build(n, "!=") } -//Lt means less than(<) +// Lt means less than(<) type Lt map[string]interface{} -//Build implements the Comparable interface +// Build implements the Comparable interface func (l Lt) Build() ([]string, []interface{}) { return build(l, "<") } -//Lte means less than or equal(<=) +// Lte means less than or equal(<=) type Lte map[string]interface{} -//Build implements the Comparable interface +// Build implements the Comparable interface func (l Lte) Build() ([]string, []interface{}) { return build(l, "<=") } -//Gt means greater than(>) +// Gt means greater than(>) type Gt map[string]interface{} -//Build implements the Comparable interface +// Build implements the Comparable interface func (g Gt) Build() ([]string, []interface{}) { return build(g, ">") } -//Gte means greater than or equal(>=) +// Gte means greater than or equal(>=) type Gte map[string]interface{} -//Build implements the Comparable interface +// Build implements the Comparable interface func (g Gte) Build() ([]string, []interface{}) { return build(g, ">=") } -//In means in +// In means in type In map[string][]interface{} -//Build implements the Comparable interface +// Build implements the Comparable interface func (i In) Build() ([]string, []interface{}) { if nil == i || 0 == len(i) { return nil, nil @@ -199,10 +199,10 @@ func buildIn(field string, vals []interface{}) (cond string) { return } -//NotIn means not in +// NotIn means not in type NotIn map[string][]interface{} -//Build implements the Comparable interface +// Build implements the Comparable interface func (i NotIn) Build() ([]string, []interface{}) { if nil == i || 0 == len(i) { return nil, nil @@ -416,6 +416,17 @@ func resolveUpdate(update map[string]interface{}) (sets string, vals []interface sb.WriteString(fmt.Sprintf("%s=%s,", k, v)) continue } + if strings.HasPrefix(k, "_custom_") { + if custom, ok := v.(Comparable); ok { + sql, val := custom.Build() + for _, s := range sql { + sb.WriteString(s) + sb.WriteByte(',') + } + vals = append(vals, val...) + } + continue + } vals = append(vals, v) sb.WriteString(fmt.Sprintf("%s=?,", quoteField(k))) } diff --git a/builder/utils.go b/builder/utils.go index f11deb7..c8fdc0b 100644 --- a/builder/utils.go +++ b/builder/utils.go @@ -4,7 +4,9 @@ import ( "context" "database/sql" "reflect" + "sort" "strconv" + "strings" ) // AggregateQuery is a helper function to execute the aggregate query and return the result @@ -174,3 +176,183 @@ func isZero(v reflect.Value) bool { } return true } + +type rawSql struct { + sqlCond string + values []interface{} +} + +func (r rawSql) Build() ([]string, []interface{}) { + return []string{r.sqlCond}, r.values +} + +func Custom(query string, args ...interface{}) Comparable { + return rawSql{sqlCond: query, values: args} +} + +// JsonContains aim to check target json contains all items in given obj;if check certain value just use direct +// where := map[string]interface{}{"your_json_field.'$.path_to_key' =": val} +// +// notice: fullJsonPath should hard code, never from user input; +// jsonLike only support json element like array,map,string,number etc., struct input will result panic!!! +// +// usage where := map[string]interface{}{"_custom_xxx": builder.JsonContains("my_json->'$.my_data.list'", 7)} +// +// usage where := map[string]interface{}{"_custom_xxx": builder.JsonContains("my_json->'$'", []int{1,2})} +// +// usage where := map[string]interface{}{"_custom_xxx": builder.JsonContains("my_json->'$.user_info'", map[string]any{"name": "", "age": 18})} +func JsonContains(fullJsonPath string, jsonLike interface{}) Comparable { + // MEMBER OF cant not deal null in json array + if jsonLike == nil { + return rawSql{ + sqlCond: "JSON_CONTAINS(" + fullJsonPath + ",'null')", + values: nil, + } + } + + s, v := genJsonObj(jsonLike) + // jsonLike is number, string, bool + _, ok := jsonLike.(string) // this check avoid eg jsonLike "JSONa" + if ok || !strings.HasPrefix(s, "JSON") { + return rawSql{ + sqlCond: "(" + s + " MEMBER OF(" + fullJsonPath + "))", + values: v, + } + } + // jsonLike is array or map + return rawSql{ + sqlCond: "JSON_CONTAINS(" + fullJsonPath + "," + s + ")", + values: v, + } +} + +// JsonSet aim to simply set/update json field operation; +// +// notice: jsonPath should hard code, never from user input; +// +// usage update := map[string]interface{}{"_custom_xxx": builder.JsonSet(field, "$.code", 1, "$.user_info", map[string]any{"name": "", "age": 18})} +func JsonSet(field string, pathAndValuePair ...interface{}) Comparable { + return jsonUpdateCall("JSON_SET", field, pathAndValuePair...) +} + +// JsonArrayAppend gen JsonObj and call MySQL JSON_ARRAY_APPEND function; +// usage update := map[string]interface{}{"_custom_xxx": builder.JsonArrayAppend(field, "$", 1, "$[last]", []string{"2","3"}} +func JsonArrayAppend(field string, pathAndValuePair ...interface{}) Comparable { + return jsonUpdateCall("JSON_ARRAY_APPEND", field, pathAndValuePair...) +} + +// JsonArrayInsert gen JsonObj and call MySQL JSON_ARRAY_INSERT function; insert at index +// usage update := map[string]interface{}{"_custom_xxx": builder.JsonArrayInsert(field, "$[0]", 1, "$[0]", []string{"2","3"}} +func JsonArrayInsert(field string, pathAndValuePair ...interface{}) Comparable { + return jsonUpdateCall("JSON_ARRAY_INSERT", field, pathAndValuePair...) +} + +// JsonRemove call MySQL JSON_REMOVE function; remove element from Array or Map +// path removed in order, prev remove affect the later operation, maybe the array shrink +// +// remove last array element; update := map[string]interface{}{"_custom_xxx":builder.JsonRemove(field,'$.list[last]')} +// remove element; update := map[string]interface{}{"_custom_xxx":builder.JsonRemove(field,'$.key0')} +func JsonRemove(field string, path ...string) Comparable { + if len(path) == 0 { + // do nothing, update xxx set a=a; + return rawSql{ + sqlCond: field + "=" + field, + values: nil, + } + } + return rawSql{ + sqlCond: field + "=JSON_REMOVE(" + field + ",'" + strings.Join(path, "','") + "')", + values: nil, + } +} + +// jsonUpdateCall build args then call fn +func jsonUpdateCall(fn string, field string, pathAndValuePair ...interface{}) Comparable { + if len(pathAndValuePair) == 0 || len(pathAndValuePair)%2 != 0 { + return rawSql{sqlCond: field, values: nil} + } + val := make([]interface{}, 0, len(pathAndValuePair)/2) + var buf strings.Builder + buf.WriteString(field) + buf.WriteByte('=') + buf.WriteString(fn + "(") + buf.WriteString(field) + for i := 0; i < len(pathAndValuePair); i += 2 { + buf.WriteString(",'") + buf.WriteString(pathAndValuePair[i].(string)) + buf.WriteString("',") + + jsonSql, jsonVals := genJsonObj(pathAndValuePair[i+1]) + buf.WriteString(jsonSql) + val = append(val, jsonVals...) + } + buf.WriteByte(')') + + return rawSql{ + sqlCond: buf.String(), + values: val, + } +} + +// genJsonObj build MySQL JSON object using JSON_ARRAY, JSON_OBJECT or ?; return sql string and args +func genJsonObj(obj interface{}) (string, []interface{}) { + if obj == nil { + return "null", nil + } + rValue := reflect.Indirect(reflect.ValueOf(obj)) + rType := rValue.Kind() + var s []string + var vals []interface{} + switch rType { + case reflect.Array, reflect.Slice: + s = append(s, "JSON_ARRAY(") + length := rValue.Len() + for i := 0; i < length; i++ { + subS, subVals := genJsonObj(rValue.Index(i).Interface()) + s = append(s, subS, ",") + vals = append(vals, subVals...) + } + + if s[len(s)-1] == "," { + s[len(s)-1] = ")" + } else { // empty slice + s = append(s, ")") + } + case reflect.Map: + s = append(s, "JSON_OBJECT(") + // sort keys in map to keep generate result same. + keys := rValue.MapKeys() + sort.Slice(keys, func(i, j int) bool { + return keys[i].String() < keys[j].String() + }) + length := rValue.Len() + for i := 0; i < length; i++ { + k := keys[i] + v := rValue.MapIndex(k) + subS, subVals := genJsonObj(v.Interface()) + s = append(s, "?,", subS, ",") + vals = append(vals, k.String()) + vals = append(vals, subVals...) + } + + if s[len(s)-1] == "," { + s[len(s)-1] = ")" + } else { // empty map + s = append(s, ")") + } + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64, + reflect.String: + return "?", []interface{}{rValue.Interface()} + case reflect.Bool: + if rValue.Bool() { + return "true", nil + } + return "false", nil + default: + panic("genJsonObj not support type: " + rType.String()) + } + return strings.Join(s, ""), vals +} diff --git a/builder/utils_test.go b/builder/utils_test.go index 762665d..339212a 100644 --- a/builder/utils_test.go +++ b/builder/utils_test.go @@ -4,6 +4,7 @@ import ( "context" "math" "reflect" + "strconv" "testing" "time" @@ -267,3 +268,522 @@ func TestOmitEmpty(t *testing.T) { ass.True(reflect.DeepEqual(tc.finalWhere, r)) } } + +func TestCustom(t *testing.T) { + // Custom in where + type inStruct struct { + table string + where map[string]interface{} + fields []string + } + type outStruct struct { + cond string + vals []interface{} + err error + } + var data = []struct { + in inStruct + out outStruct + }{ + { + in: inStruct{ + table: "tb", + where: map[string]interface{}{ + "foo": "bar", + "_custom_1": Custom("(x=? OR y=?)", 20, "1"), + "age in": []interface{}{1, 3, 5, 7, 9}, + "vx": []interface{}{1, 3, 5}, + "faith <>": "Muslim", + "_or": []map[string]interface{}{ + { + "aa": 11, + "bb": "xswl", + }, + { + "cc": "234", + "dd in": []interface{}{7, 8}, + "_or": []map[string]interface{}{ + { + "neeest_ee <>": "dw42", + "neeest_ff in": []interface{}{34, 59}, + }, + { + "neeest_gg": 1259, + "neeest_hh not in": []interface{}{358, 1245}, + }, + }, + }, + }, + "_orderby": "age DESC,score ASC", + "_groupby": "department", + "_limit": []uint{0, 100}, + "_custom_2": Custom("(xx=? AND yy=?)", 20, "2"), + }, + fields: []string{"id", "name", "age"}, + }, + out: outStruct{ + cond: "SELECT id,name,age FROM tb WHERE ((x=? OR y=?) AND (xx=? AND yy=?) AND ((aa=? AND bb=?) OR (((neeest_ff IN (?,?) AND neeest_ee!=?) OR (neeest_gg=? AND neeest_hh NOT IN (?,?))) AND cc=? AND dd IN (?,?))) AND foo=? AND age IN (?,?,?,?,?) AND vx IN (?,?,?) AND faith!=?) GROUP BY department ORDER BY age DESC,score ASC LIMIT ?,?", + vals: []interface{}{20, "1", 20, "2", 11, "xswl", 34, 59, "dw42", 1259, 358, 1245, "234", 7, 8, "bar", 1, 3, 5, 7, 9, 1, 3, 5, "Muslim", 0, 100}, + err: nil, + }, + }, + { + in: inStruct{ + table: "tb", + where: map[string]interface{}{ + "name like": "%123", + "_custom_1": Custom("(x=? OR y=?)", 20, "1"), + }, + fields: nil, + }, + out: outStruct{ + cond: `SELECT * FROM tb WHERE ((x=? OR y=?) AND name LIKE ?)`, + vals: []interface{}{20, "1", "%123"}, + err: nil, + }, + }, + + { + in: inStruct{ + table: "tb", + where: map[string]interface{}{ + "foo": "bar", + "_custom_1": Custom("(x=? OR y=?)", 20, "1"), + "_orderby": " ", + }, + fields: nil, + }, + out: outStruct{ + cond: "SELECT * FROM tb WHERE ((x=? OR y=?) AND foo=?)", + vals: []interface{}{20, "1", "bar"}, + err: nil, + }, + }, + { + in: inStruct{ + table: "tb", + where: map[string]interface{}{ + "_custom_0": JsonContains("my_json->'$.data.list'", []int{1, 0}), + "_custom_12": Custom("x=?", 20), + "_custom_1": Custom("(age=? OR name=?)", 20, "test"), + }, + fields: nil, + }, + out: outStruct{ + cond: "SELECT * FROM tb WHERE (JSON_CONTAINS(my_json->'$.data.list',JSON_ARRAY(?,?)) AND (age=? OR name=?) AND x=?)", + vals: []interface{}{1, 0, 20, "test", 20}, + err: nil, + }, + }, + } + ass := assert.New(t) + for _, tc := range data { + cond, vals, err := BuildSelect(tc.in.table, tc.in.where, tc.in.fields) + ass.Equal(tc.out.err, err) + ass.Equal(tc.out.cond, cond) + ass.Equal(tc.out.vals, vals) + } + + // Custom in update + { + update := map[string]interface{}{ + "name": "name000", + "_custom_": Custom("a=a*?,aa=999", 10), + } + where := map[string]interface{}{ + "id": 5, + } + cond, vals, err := BuildUpdate("xx", where, update) + ass.NoError(err) + ass.Equal( + "UPDATE xx SET a=a*?,aa=999,name=? WHERE (id=?)", + cond) + ass.Equal( + []interface{}{10, "name000", 5}, + vals) + } + + // Custom both in update and where + { + update := map[string]interface{}{ + "name": "name000", + "_custom_": Custom("a=a*?,aa=999", 10), + } + where := map[string]interface{}{ + "_custom_": Custom("json_contains(my_json,cast(? as json))", 10), + "name !=": "", + } + cond, vals, err := BuildUpdate("xx", where, update) + ass.NoError(err) + ass.Equal( + "UPDATE xx SET a=a*?,aa=999,name=? WHERE (json_contains(my_json,cast(? as json)) AND name!=?)", + cond) + ass.Equal( + []interface{}{10, "name000", 10, ""}, + vals) + } +} + +func TestGenJsonObj(t *testing.T) { + json := map[string]interface{}{ + "a": "--a", + "b": 1.5, + "c": []map[string]interface{}{ + {"a": "ca0", "b": 2, "c": nil}, + {"a": "ca1", "b": 1, "c": []map[string]interface{}{ + {"a": ";", "b": true}, + }, + }, + }, + "d": map[string]interface{}{ + "a": "da", + }, + } + + validMapOrder := make(map[string]int, 11) + for i := 0; i < 11; i++ { + validMapOrder["k"+strconv.Itoa(i)] = i + } + + const testCount = 100 + testData := []struct { + in interface{} + outSql string + outVal []interface{} + }{ + {18, "?", []interface{}{18}}, + {false, "false", []interface{}(nil)}, + {nil, "null", []interface{}(nil)}, + {[]int{1, 2, 3}, "JSON_ARRAY(?,?,?)", []interface{}{1, 2, 3}}, + {validMapOrder, + "JSON_OBJECT(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", + []interface{}{"k0", 0, "k1", 1, "k10", 10, "k2", 2, "k3", 3, "k4", 4, "k5", 5, "k6", 6, "k7", 7, "k8", 8, "k9", 9}, + }, + {json, + "JSON_OBJECT(?,?,?,?,?,JSON_ARRAY(JSON_OBJECT(?,?,?,?,?,null),JSON_OBJECT(?,?,?,?,?,JSON_ARRAY(JSON_OBJECT(?,?,?,true)))),?,JSON_OBJECT(?,?))", + []interface{}{"a", "--a", "b", 1.5, "c", "a", "ca0", "b", 2, "c", "a", "ca1", "b", 1, "c", "a", ";", "b", "d", "a", "da"}, + }, + } + ass := assert.New(t) + for i := 0; i < testCount; i++ { + for _, v := range testData { + sql, val := genJsonObj(v.in) + ass.Equal(v.outSql, sql) + ass.Equal(v.outVal, val) + } + } +} + +func TestJsonArrayAppend(t *testing.T) { + type inStruct struct { + table string + update map[string]interface{} + where map[string]interface{} + } + type outStruct struct { + cond string + vals []interface{} + err error + } + var testData = []struct { + in inStruct + out outStruct + }{ + { + in: inStruct{ + table: "tb", + update: map[string]interface{}{ + "_custom_0": JsonArrayAppend("my_json", "$", 0, "$", 1), + "name": "newName", + }, + where: map[string]interface{}{ + "id": 3, + }, + }, + out: outStruct{ + cond: "UPDATE tb SET my_json=JSON_ARRAY_APPEND(my_json,'$',?,'$',?),name=? WHERE (id=?)", + vals: []interface{}{0, 1, "newName", 3}, + err: nil, + }, + }, + { + in: inStruct{ + table: "tb", + update: map[string]interface{}{ + "_custom_0": JsonArrayAppend("my_json", "$", 0, "$[0]", []int{7, 8, 9}), + }, + where: map[string]interface{}{ + "id": []int{4, 5, 6}, + }, + }, + out: outStruct{ + cond: "UPDATE tb SET my_json=JSON_ARRAY_APPEND(my_json,'$',?,'$[0]',JSON_ARRAY(?,?,?)) WHERE (id IN (?,?,?))", + vals: []interface{}{0, 7, 8, 9, 4, 5, 6}, + err: nil, + }, + }, + } + ass := assert.New(t) + + for _, v := range testData { + sql, val, err := BuildUpdate(v.in.table, v.in.where, v.in.update) + ass.Equal(v.out.cond, sql) + ass.Equal(v.out.vals, val) + ass.NoError(err) + } + +} + +func TestJsonSet(t *testing.T) { + type inStruct struct { + table string + update map[string]interface{} + where map[string]interface{} + } + type outStruct struct { + cond string + vals []interface{} + err error + } + var testData = []struct { + in inStruct + out outStruct + }{ + { + in: inStruct{ + table: "xx", + update: map[string]interface{}{ + "_custom_0": JsonSet("my_json", "$.a", 0, "$.friend_id", []int{7, 8, 9}), + "name": "newName", + }, + where: map[string]interface{}{ + "id": 8, + }, + }, + out: outStruct{ + cond: "UPDATE xx SET my_json=JSON_SET(my_json,'$.a',?,'$.friend_id',JSON_ARRAY(?,?,?)),name=? WHERE (id=?)", + vals: []interface{}{0, 7, 8, 9, "newName", 8}, + err: nil, + }, + }, + { + in: inStruct{ + table: "xx", + update: map[string]interface{}{ + "_custom_0": JsonSet("my_json", "$[0]", []int{7, 8, 9}), + }, + where: map[string]interface{}{ + "id": []int{4, 5, 6}, + }, + }, + out: outStruct{ + cond: "UPDATE xx SET my_json=JSON_SET(my_json,'$[0]',JSON_ARRAY(?,?,?)) WHERE (id IN (?,?,?))", + vals: []interface{}{7, 8, 9, 4, 5, 6}, + err: nil, + }, + }, + } + ass := assert.New(t) + + for _, v := range testData { + sql, val, err := BuildUpdate(v.in.table, v.in.where, v.in.update) + ass.Equal(v.out.cond, sql) + ass.Equal(v.out.vals, val) + ass.NoError(err) + } + +} + +func TestJsonContains(t *testing.T) { + testData := []struct { + fullJsonPath string + jsonLike interface{} + + outSql string + outVal []interface{} + }{ + { + fullJsonPath: "my_json->'$'", + jsonLike: 18, + outSql: "(? MEMBER OF(my_json->'$'))", + outVal: []interface{}{18}, + }, + { + fullJsonPath: "my_json->'$'", + jsonLike: []interface{}{18, "xxx"}, + outSql: "JSON_CONTAINS(my_json->'$',JSON_ARRAY(?,?))", + outVal: []interface{}{18, "xxx"}, + }, + { + fullJsonPath: "my_json", + jsonLike: true, + outSql: "(true MEMBER OF(my_json))", + outVal: nil, + }, + { + fullJsonPath: "my_json->'$'", + jsonLike: nil, + outSql: "JSON_CONTAINS(my_json->'$','null')", + outVal: nil, + }, + { + fullJsonPath: "my_json->'$.friend'", + jsonLike: map[string]interface{}{"name": "A", "age": 88}, + outSql: "JSON_CONTAINS(my_json->'$.friend',JSON_OBJECT(?,?,?,?))", + outVal: []interface{}{"age", 88, "name", "A"}, + }, + } + ass := assert.New(t) + + for _, v := range testData { + sql, val := JsonContains(v.fullJsonPath, v.jsonLike).Build() + ass.Equal(v.outSql, sql[0]) + ass.Equal(v.outVal, val) + } +} + +func TestJsonRemove(t *testing.T) { + type inStruct struct { + table string + update map[string]interface{} + where map[string]interface{} + } + type outStruct struct { + cond string + vals []interface{} + err error + } + var testData = []struct { + in inStruct + out outStruct + }{ + { + in: inStruct{ + table: "xx", + update: map[string]interface{}{ + "_custom_0": JsonRemove("my_json", "$.unused", "$.list[last]"), + "name": "newName", + }, + where: map[string]interface{}{ + "_custom_0": JsonContains("my_json->'$.list[last]'", nil), + }, + }, + out: outStruct{ + cond: "UPDATE xx SET my_json=JSON_REMOVE(my_json,'$.unused','$.list[last]'),name=? WHERE (JSON_CONTAINS(my_json->'$.list[last]','null'))", + vals: []interface{}{"newName"}, + err: nil, + }, + }, + { + in: inStruct{ + table: "xx", + update: map[string]interface{}{ + "_custom_0": JsonRemove("my_json"), + }, + where: map[string]interface{}{ + "id": []int{4, 5, 6}, + }, + }, + out: outStruct{ + cond: "UPDATE xx SET my_json=my_json WHERE (id IN (?,?,?))", + vals: []interface{}{4, 5, 6}, + err: nil, + }, + }, + } + ass := assert.New(t) + + for _, v := range testData { + sql, val, err := BuildUpdate(v.in.table, v.in.where, v.in.update) + ass.Equal(v.out.cond, sql) + ass.Equal(v.out.vals, val) + ass.NoError(err) + } + +} + +func TestJsonArrayInsert(t *testing.T) { + type inStruct struct { + table string + update map[string]interface{} + where map[string]interface{} + } + type outStruct struct { + cond string + vals []interface{} + err error + } + var testData = []struct { + in inStruct + out outStruct + }{ + { + in: inStruct{ + table: "xx", + update: map[string]interface{}{ + "a": 18, + "_custom_0": JsonArrayInsert("my_json", "$[0]", "first", "$[1]", 2), + "name": "newName", + }, + where: map[string]interface{}{ + "_or": []map[string]interface{}{ + {"_custom_0": JsonContains("my_json->'$[0]'", 1)}, + {"_custom_0": JsonContains("my_json->'$[0]'", 7)}, + }, + }, + }, + out: outStruct{ + cond: "UPDATE xx SET my_json=JSON_ARRAY_INSERT(my_json,'$[0]',?,'$[1]',?),a=?,name=? WHERE ((((? MEMBER OF(my_json->'$[0]'))) OR ((? MEMBER OF(my_json->'$[0]')))))", + vals: []interface{}{"first", 2, 18, "newName", 1, 7}, + err: nil, + }, + }, + { + in: inStruct{ + table: "xx", + update: map[string]interface{}{ + "_custom_0": JsonArrayInsert("my_json", "$[0]", 0, "$[0]", []int{7, 8, 9}), + }, + where: map[string]interface{}{ + "id": []int{4, 5, 6}, + }, + }, + out: outStruct{ + cond: "UPDATE xx SET my_json=JSON_ARRAY_INSERT(my_json,'$[0]',?,'$[0]',JSON_ARRAY(?,?,?)) WHERE (id IN (?,?,?))", + vals: []interface{}{0, 7, 8, 9, 4, 5, 6}, + err: nil, + }, + }, + } + ass := assert.New(t) + + for _, v := range testData { + cond, val, err := BuildUpdate(v.in.table, v.in.where, v.in.update) + ass.Equal(v.out.cond, cond) + ass.Equal(v.out.vals, val) + ass.NoError(err) + } +} + +func TestOrAndCustomJsonContains(t *testing.T) { + where := map[string]interface{}{ + "_or1": []map[string]interface{}{{"a": 1}, {"b": 1}}, + "_or2": []map[string]interface{}{{"aa": 1}, {"bb": 1}}, + "_or3": []map[string]interface{}{{"aaa": 1}, {"bbb": 1}}, + "_custom_0": JsonContains("my_json", []int(nil)), + "_custom_01": JsonContains("my_json", map[string]interface{}{}), + "_custom_1": JsonContains("my_json->'$'", 8), + "_custom_3": JsonContains("my_json->'$[last]'", map[string]interface{}{"a": 1, "b": 2}), + "_custom_4": JsonContains("my_json", []int{1, 3}), + "_custom_5": JsonContains("my_json->'$[0]'", nil), + } + s := "SELECT * FROM xx WHERE (JSON_CONTAINS(my_json,JSON_ARRAY()) AND JSON_CONTAINS(my_json,JSON_OBJECT()) AND (? MEMBER OF(my_json->'$')) AND JSON_CONTAINS(my_json->'$[last]',JSON_OBJECT(?,?,?,?)) AND JSON_CONTAINS(my_json,JSON_ARRAY(?,?)) AND JSON_CONTAINS(my_json->'$[0]','null') AND ((a=?) OR (b=?)) AND ((aa=?) OR (bb=?)) AND ((aaa=?) OR (bbb=?)))" + v := []interface{}{8, "a", 1, "b", 2, 1, 3, 1, 1, 1, 1, 1, 1} + ass := assert.New(t) + + s1, v1, err := BuildSelect("xx", where, nil) + ass.Equal(s, s1) + ass.Equal(v, v1) + ass.NoError(err) + +}