diff --git a/README.md b/README.md index b0f49e6..5dca9a0 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The following operators are added: (Credit goes to Ahmad Baitalmal) * isnot null Previously I was hoping to make the query as similar to SQL `WHERE` clause as possible. Later I found a problem parsing the term `PRIORITY>5`. The tokenizer split it as `PRI`, `OR`, `ITY`, `>`, `5`, since `OR` was then an operator, which is terribly wrong. At that point, I thought of two choices: -1. to force the query expression to contain at least one white space between tokens, thus `PRIORITY>5` should be written as `PRIORITY > 5`; +1. to force the query expression to contain at least one white space between tokens, thus `PRIORITY>5` should be written as `PRIORITY > 5`; 2. to replace operators as follows: * `AND` to `&&` diff --git a/jsonql_test.go b/jsonql_test.go index 054cb2b..d6e5c04 100644 --- a/jsonql_test.go +++ b/jsonql_test.go @@ -16,7 +16,9 @@ func TestParse(t *testing.T) { "Golang", "Java", "C" - ] + ], + "comments": "", + "score": 40 }, { "name": "enny", @@ -25,7 +27,10 @@ func TestParse(t *testing.T) { "IC", "Electric design", "Verification" - ] + ], + "comments": "2nd record", + "score": 10 + }, { "name": "sam", @@ -34,7 +39,9 @@ func TestParse(t *testing.T) { "Eating", "Sleeping", "Crawling" - ] + ], + "comments": "3rd record", + "score": 30 } ] ` @@ -49,7 +56,20 @@ func TestParse(t *testing.T) { }{ {"name='elgs'", 1}, {"gender='f'", 1}, + {"comments='2nd record'", 1}, + {"comments=\"\"", 1}, + {"comments=\"\"", 1}, {"skills.[1]='Sleeping'", 1}, + {"skills contains 'Sleeping'", 1}, + {"skills contains 'Awake'", 0}, + {"name contains 'lgs'", 1}, + {"name contains 'e'", 2}, + {"'Sleeping' in skills", 1}, + {"'Verification' notin skills", 2}, + {"'Awake' in skills", 0}, + {"'lgs' in name", 1}, + {"'e' in name", 2}, + {"score > 10 && score < 40", 1}, } var fail = []struct { in string @@ -61,9 +81,13 @@ func TestParse(t *testing.T) { t.Error(v.in, err) } fmt.Println(v.in, result) - // if v.ex != result { - // t.Error("Expected:", v.ex, "actual:", result) - // } + numResults, ok := result.([]interface{}) + if !ok { + t.Error("Failed to convert result") + } + if len(numResults) != v.ex { + t.Errorf("Got %v results, expected %v for `%v`", len(numResults), v.ex, v.in) + } } for range fail { diff --git a/sql_operators.go b/sql_operators.go index 38fd524..0a6b9c0 100644 --- a/sql_operators.go +++ b/sql_operators.go @@ -32,8 +32,48 @@ func evalToken(symbolTable interface{}, token string) (interface{}, error) { return jq.Query(token) } +func opIn(symbolTable interface{}, left string, right string) (string, error) { + l, lUndefined := evalToken(symbolTable, left) + if lUndefined != nil { + return "false", nil + } + ls := l.(string) + + r, rUndefined := evalToken(symbolTable, right) + if rUndefined != nil { + return "false", nil + } + + switch r.(type) { + case []interface{}: + jsn, _ := r.([]interface{}) + for _, j := range jsn { + if s, ok := j.(string); ok { + if s == ls { + return "true", nil + } + } + } + return "false", nil + case interface{}: + str, ok := r.(string) + if !ok { + return "false", nil + } + contains := strings.Contains(str, ls) + if contains { + return "true", nil + } + return "false", nil + + default: + return "false", nil + } + +} + var sqlOperators = map[string]*Operator{ - // Tokenizer will be responsible to put a space before and after each ')OR(', but not priORity. + // Tokenizer will be responsible to put a space before and after each ')OR(', but not priority. "||": { Precedence: 1, Eval: func(symbolTable interface{}, left string, right string) (string, error) { @@ -62,6 +102,62 @@ var sqlOperators = map[string]*Operator{ return strconv.FormatBool(l && r), nil }, }, + "contains": { + Precedence: 6, + Eval: func(symbolTable interface{}, left string, right string) (string, error) { + l, lUndefined := evalToken(symbolTable, left) + if lUndefined != nil { + return "false", nil + } + r, rUndefined := evalToken(symbolTable, right) + if rUndefined != nil { + return "false", nil + } + rs := r.(string) + + switch l.(type) { + case []interface{}: + jsn, _ := l.([]interface{}) + for _, j := range jsn { + if s, ok := j.(string); ok { + if s == rs { + return "true", nil + } + } + } + return "false", nil + case interface{}: + str, ok := l.(string) + if !ok { + return "false", nil + } + contains := strings.Contains(str, rs) + if contains { + return "true", nil + } + return "false", nil + + default: + return "false", nil + } + + }, + }, + "in": { + Precedence: 6, + Eval: opIn, + }, + "notin": { + Precedence: 6, + Eval: func(symbolTable interface{}, left string, right string) (string, error) { + s, err := opIn(symbolTable, left, right) + if err != nil { + return s, err + } + b, _ := strconv.ParseBool(s) + return strconv.FormatBool(!b), err + }, + }, "is": { Precedence: 5, Eval: func(symbolTable interface{}, left string, right string) (string, error) {