From 3a173cd7d9d23f3c038bcf80f1ca6032e2bff1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Portebois?= Date: Mon, 1 Apr 2019 21:09:43 -0400 Subject: [PATCH 1/3] Add support for in and contains --- jsonql_test.go | 18 ++++++++++-- sql_operators.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/jsonql_test.go b/jsonql_test.go index 054cb2b..6992585 100644 --- a/jsonql_test.go +++ b/jsonql_test.go @@ -50,6 +50,14 @@ func TestParse(t *testing.T) { {"name='elgs'", 1}, {"gender='f'", 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}, + {"Awake in skills", 0}, + {"lgs in name", 1}, + {"e in name", 2}, } var fail = []struct { in string @@ -61,9 +69,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", len(numResults), v.ex) + } } for range fail { diff --git a/sql_operators.go b/sql_operators.go index 38fd524..a85c036 100644 --- a/sql_operators.go +++ b/sql_operators.go @@ -62,6 +62,81 @@ 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 + } + + switch l.(type) { + case []interface{}: + jsn, _ := l.([]interface{}) + for _, j := range jsn { + if s, ok := j.(string); ok { + if s == right { + return "true", nil + } + } + + } + return "false", nil + case interface{}: + str, ok := l.(string) + if !ok { + return "false", nil + } + contains := strings.Contains(str, right) + if contains { + return "true", nil + } + return "false", nil + + default: + return "false", nil + } + + }, + }, + "in": { + Precedence: 6, + Eval: func(symbolTable interface{}, left string, right string) (string, error) { + 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 == left { + return "true", nil + } + } + + } + return "false", nil + case interface{}: + str, ok := r.(string) + if !ok { + return "false", nil + } + contains := strings.Contains(str, left) + if contains { + return "true", nil + } + return "false", nil + + default: + return "false", nil + } + + }, + }, "is": { Precedence: 5, Eval: func(symbolTable interface{}, left string, right string) (string, error) { From 93e902546a6e07a1081f8c5b30b27665716872de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Portebois?= Date: Tue, 2 Apr 2019 21:25:15 -0400 Subject: [PATCH 2/3] Fix in sql operators for lists --- sql_operators.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/sql_operators.go b/sql_operators.go index a85c036..fbd32fc 100644 --- a/sql_operators.go +++ b/sql_operators.go @@ -65,22 +65,25 @@ var sqlOperators = map[string]*Operator{ "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 == right { + if s == rs { return "true", nil } } - } return "false", nil case interface{}: @@ -88,7 +91,7 @@ var sqlOperators = map[string]*Operator{ if !ok { return "false", nil } - contains := strings.Contains(str, right) + contains := strings.Contains(str, rs) if contains { return "true", nil } @@ -103,6 +106,12 @@ var sqlOperators = map[string]*Operator{ "in": { Precedence: 6, Eval: func(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 @@ -113,11 +122,10 @@ var sqlOperators = map[string]*Operator{ jsn, _ := r.([]interface{}) for _, j := range jsn { if s, ok := j.(string); ok { - if s == left { + if s == ls { return "true", nil } } - } return "false", nil case interface{}: @@ -125,7 +133,7 @@ var sqlOperators = map[string]*Operator{ if !ok { return "false", nil } - contains := strings.Contains(str, left) + contains := strings.Contains(str, ls) if contains { return "true", nil } From 9ca167a9469f44d3323aabeca8d9128e8cad5151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Portebois?= Date: Thu, 11 Apr 2019 20:48:04 -0400 Subject: [PATCH 3/3] Add notin in sqloperators --- README.md | 2 +- jsonql_test.go | 36 +++++++++++++------- sql_operators.go | 87 ++++++++++++++++++++++++++++-------------------- 3 files changed, 75 insertions(+), 50 deletions(-) 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 6992585..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,15 +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}, - {"Awake in skills", 0}, - {"lgs in name", 1}, - {"e in name", 2}, + {"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 @@ -74,7 +86,7 @@ func TestParse(t *testing.T) { t.Error("Failed to convert result") } if len(numResults) != v.ex { - t.Errorf("Got %v results, expected %v", 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 fbd32fc..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) { @@ -104,45 +144,18 @@ var sqlOperators = map[string]*Operator{ }, }, "in": { + Precedence: 6, + Eval: opIn, + }, + "notin": { Precedence: 6, Eval: func(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 + s, err := opIn(symbolTable, left, right) + if err != nil { + return s, err } - + b, _ := strconv.ParseBool(s) + return strconv.FormatBool(!b), err }, }, "is": {