Skip to content

Commit 11d2b30

Browse files
committed
[add] initial version
0 parents  commit 11d2b30

File tree

5 files changed

+305
-0
lines changed

5 files changed

+305
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
JSON to SQL Query
2+
==================
3+
4+
Converts JSON config to SQL Query
5+
6+
```json
7+
{
8+
"glue": "and",
9+
"data": [{
10+
"field":"age",
11+
"condition":{
12+
"rule": "less",
13+
"value": 42
14+
}
15+
},{
16+
"field":"region",
17+
"includes": [1,2,6]
18+
}]
19+
}
20+
```
21+

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/xbsoftware/querysql
2+
3+
go 1.13

sql.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package querysql
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
type Filter struct {
10+
Glue string `json:"glue"`
11+
Field string `json:"field"`
12+
Condition Condition `json:"condition"`
13+
Includes []interface{} `json:"includes"`
14+
Kids []Filter `json:"data"`
15+
}
16+
17+
type Condition struct {
18+
Rule string `json:"rule"`
19+
Value interface{} `json:"value"`
20+
}
21+
22+
type CustomOperation func(string, Condition) (string, []interface{}, error)
23+
24+
type SQLConfig struct {
25+
Whitelist map[string]bool
26+
Operations map[string]CustomOperation
27+
}
28+
29+
func FromJSON(text []byte) (Filter, error) {
30+
f := Filter{}
31+
err := json.Unmarshal(text, &f)
32+
return f, err
33+
}
34+
35+
var NoValues = make([]interface{}, 0)
36+
37+
func inSQL(field string, data []interface{}) (string, []interface{}, error) {
38+
marks := make([]string, len(data))
39+
for i := range marks {
40+
marks[i] = "?"
41+
}
42+
43+
sql := fmt.Sprintf("%s IN(%s)", field, strings.Join(marks, ","))
44+
return sql, data, nil
45+
}
46+
47+
func GetSQL(data Filter, config *SQLConfig) (string, []interface{}, error) {
48+
if data.Kids == nil {
49+
if config != nil && config.Whitelist != nil && !config.Whitelist[data.Field] {
50+
return "", nil, fmt.Errorf("field name is not in whitelist: %s", data.Field)
51+
}
52+
53+
if len(data.Includes) > 0 {
54+
return inSQL(data.Field, data.Includes)
55+
}
56+
57+
switch data.Condition.Rule {
58+
case "":
59+
return "", NoValues, nil
60+
case "equal":
61+
return fmt.Sprintf("%s = ?", data.Field), []interface{}{data.Condition.Value}, nil
62+
case "less":
63+
return fmt.Sprintf("%s < ?", data.Field), []interface{}{data.Condition.Value}, nil
64+
case "greater":
65+
return fmt.Sprintf("%s > ?", data.Field), []interface{}{data.Condition.Value}, nil
66+
}
67+
68+
if config.Operations != nil {
69+
op, opOk := config.Operations[data.Condition.Rule]
70+
if opOk {
71+
return op(data.Field, data.Condition)
72+
}
73+
}
74+
75+
return "", NoValues, fmt.Errorf("unknown operation: %s", data.Condition.Rule)
76+
}
77+
78+
out := make([]string, 0, len(data.Kids))
79+
values := make([]interface{}, 0)
80+
81+
for _, r := range data.Kids {
82+
subSql, subValues, err := GetSQL(r, config)
83+
if err != nil {
84+
return "", nil, err
85+
}
86+
out = append(out, subSql)
87+
values = append(values, subValues...)
88+
}
89+
90+
var glue string
91+
if data.Glue == "or" {
92+
glue = " OR "
93+
} else {
94+
glue = " AND "
95+
}
96+
97+
outStr := strings.Join(out, glue)
98+
if len(data.Kids) > 1 {
99+
outStr = "( " + outStr + " )"
100+
}
101+
102+
return outStr, values, nil
103+
}

sql_test.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package querysql
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
"testing"
8+
)
9+
10+
var aAndB = `{ "glue":"and", "data":[{ "field": "a", "condition":{ "rule":"less", "value":1}}, { "field": "b", "condition":{ "rule":"greater", "value":"abc" }}]}`
11+
var aOrB = `{ "glue":"or", "data":[{ "field": "a", "condition":{ "rule":"less", "value":1}}, { "field": "b", "condition":{ "rule":"greater", "value":"abc" }}]}`
12+
var cOrC = `{ "glue":"or", "data":[{ "field": "a", "condition":{ "rule":"is null" }}, { "field": "b", "condition":{ "rule":"range100", "value":500 }}]}`
13+
14+
var cases = [][]string{
15+
[]string{`{}`, "", ""},
16+
[]string{
17+
`{ "glue":"and", "data":[{ "field": "a", "condition":{ "rule":"equal", "value":1 }}]}`,
18+
"a = ?",
19+
"1",
20+
},
21+
[]string{
22+
aAndB,
23+
"( a < ? AND b > ? )",
24+
"1,abc",
25+
},
26+
[]string{
27+
aOrB,
28+
"( a < ? OR b > ? )",
29+
"1,abc",
30+
},
31+
[]string{
32+
`{ "glue":"AND", "data":[` + aAndB + `,` + aOrB + `,{ "field":"c", "condition": { "rule":"equal", "value":3 } }]}`,
33+
"( ( a < ? AND b > ? ) AND ( a < ? OR b > ? ) AND c = ? )",
34+
"1,abc,1,abc,3",
35+
},
36+
[]string{
37+
`{ "glue":"and", "data":[{ "field": "a", "includes":[1,2,3]}]}`,
38+
"a IN(?,?,?)",
39+
"1,2,3",
40+
},
41+
[]string{
42+
`{ "glue":"and", "data":[{ "field": "a", "includes":["a","b","c"]}]}`,
43+
"a IN(?,?,?)",
44+
"a,b,c",
45+
},
46+
}
47+
48+
func anyToStringArray(some []interface{}) (string, error) {
49+
out := make([]string, 0, len(some))
50+
for _, x := range some {
51+
str, strOk := x.(string)
52+
if strOk {
53+
out = append(out, str)
54+
continue
55+
}
56+
57+
num, numOk := x.(float64)
58+
if numOk {
59+
out = append(out, strconv.Itoa(int(num)))
60+
continue
61+
}
62+
63+
return "", fmt.Errorf("can't convert %+v to a string", x)
64+
}
65+
66+
return strings.Join(out, ","), nil
67+
}
68+
69+
func TestSQL(t *testing.T) {
70+
for _, line := range cases {
71+
format, err := FromJSON([]byte(line[0]))
72+
if err != nil {
73+
t.Errorf("can't parse json\nj: %s\n%f", line[0], err)
74+
continue
75+
}
76+
77+
sql, vals, err := GetSQL(format, nil)
78+
if err != nil {
79+
t.Errorf("can't generate sql\nj: %s\n%f", line[0], err)
80+
continue
81+
}
82+
if sql != line[1] {
83+
t.Errorf("wrong sql generated\nj: %s\ns: %s\nr: %s", line[0], line[1], sql)
84+
continue
85+
}
86+
87+
valsStr, err := anyToStringArray(vals)
88+
if err != nil {
89+
t.Errorf("can't convert parameters\nj: %s\n%f", line[0], err)
90+
continue
91+
}
92+
93+
if valsStr != line[2] {
94+
t.Errorf("wrong sql generated\nj: %s\ns: %s\nr: %s", line[0], line[2], valsStr)
95+
continue
96+
}
97+
}
98+
}
99+
100+
func TestWhitelist(t *testing.T) {
101+
format, err := FromJSON([]byte(aAndB))
102+
if err != nil {
103+
t.Errorf("can't parse json\nj: %s\n%f", aAndB, err)
104+
return
105+
}
106+
107+
_, _, err = GetSQL(format, nil)
108+
if err != nil {
109+
t.Errorf("doesn't work without config")
110+
return
111+
}
112+
_, _, err = GetSQL(format, &SQLConfig{})
113+
if err != nil {
114+
t.Errorf("doesn't work without whitelist")
115+
return
116+
}
117+
118+
_, _, err = GetSQL(format, &SQLConfig{Whitelist: map[string]bool{"a": true, "b": true}})
119+
if err != nil {
120+
t.Errorf("doesn't work with fields allowed")
121+
return
122+
}
123+
124+
_, _, err = GetSQL(format, &SQLConfig{Whitelist: map[string]bool{"a": true}})
125+
if err == nil {
126+
t.Errorf("doesn't return error when field is not allowed")
127+
return
128+
}
129+
130+
_, _, err = GetSQL(format, &SQLConfig{Whitelist: map[string]bool{"b": true}})
131+
if err == nil {
132+
t.Errorf("doesn't return error when field is not allowed")
133+
return
134+
}
135+
}
136+
137+
func TestCustomOperation(t *testing.T) {
138+
format, err := FromJSON([]byte(cOrC))
139+
if err != nil {
140+
t.Errorf("can't parse json\nj: %s\n%f", aAndB, err)
141+
return
142+
}
143+
144+
sql, vals, err := GetSQL(format, &SQLConfig{
145+
Operations: map[string]CustomOperation{
146+
"is null" : func(n string, c Condition) (string, []interface{}, error) {
147+
return fmt.Sprintf("%s IS NULL", n), NoValues, nil
148+
},
149+
"range100" : func(n string, c Condition) (string, []interface{}, error) {
150+
return fmt.Sprintf("( %s > ? AND %s < ? + 100 )", n, n), []interface{}{c.Value, c.Value}, nil
151+
},
152+
},
153+
})
154+
155+
if err != nil {
156+
t.Errorf("can't generate sql: %s\n%f", cOrC, err)
157+
return
158+
}
159+
160+
check := "( a IS NULL OR ( b > ? AND b < ? + 100 ) )"
161+
if sql != check {
162+
t.Errorf("wrong sql generated\nj: %s\ns: %s\nr: %s", cOrC, check, sql)
163+
return
164+
}
165+
166+
valsStr, err := anyToStringArray(vals)
167+
if err != nil {
168+
t.Errorf("can't convert parameters\nj: %s\n%f", cOrC, err)
169+
return
170+
}
171+
172+
check = "500,500"
173+
if valsStr != check {
174+
t.Errorf("wrong sql generated\nj: %s\ns: %s\nr: %s", cOrC, check, valsStr)
175+
return
176+
}
177+
}

0 commit comments

Comments
 (0)