Add new filters type in the query of the state component (#3218)
Signed-off-by: Luigi Rende <luigirende@gmail.com> Co-authored-by: Bernd Verst <github@bernd.dev>
This commit is contained in:
parent
87aea87e95
commit
a7c64f4e8d
|
@ -40,6 +40,46 @@ func (q *Query) VisitEQ(f *query.EQ) (string, error) {
|
|||
return q.whereFieldEqual(f.Key, f.Val), nil
|
||||
}
|
||||
|
||||
func (q *Query) VisitNEQ(f *query.NEQ) (string, error) {
|
||||
return q.whereFieldNotEqual(f.Key, f.Val), nil
|
||||
}
|
||||
|
||||
func (q *Query) VisitGT(f *query.GT) (string, error) {
|
||||
switch v := f.Val.(type) {
|
||||
case string:
|
||||
return "", fmt.Errorf("unsupported type of value %s; string type not permitted", v)
|
||||
default:
|
||||
return q.whereFieldGreaterThan(f.Key, v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitGTE(f *query.GTE) (string, error) {
|
||||
switch v := f.Val.(type) {
|
||||
case string:
|
||||
return "", fmt.Errorf("unsupported type of value %s; string type not permitted", v)
|
||||
default:
|
||||
return q.whereFieldGreaterThanEqual(f.Key, v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitLT(f *query.LT) (string, error) {
|
||||
switch v := f.Val.(type) {
|
||||
case string:
|
||||
return "", fmt.Errorf("unsupported type of value %s; string type not permitted", v)
|
||||
default:
|
||||
return q.whereFieldLessThan(f.Key, v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitLTE(f *query.LTE) (string, error) {
|
||||
switch v := f.Val.(type) {
|
||||
case string:
|
||||
return "", fmt.Errorf("unsupported type of value %s; string type not permitted", v)
|
||||
default:
|
||||
return q.whereFieldLessThanEqual(f.Key, v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitIN(f *query.IN) (string, error) {
|
||||
if len(f.Vals) == 0 {
|
||||
return "", fmt.Errorf("empty IN operator for key %q", f.Key)
|
||||
|
@ -70,6 +110,31 @@ func (q *Query) visitFilters(op string, filters []query.Filter) (string, error)
|
|||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.NEQ:
|
||||
if str, err = q.VisitNEQ(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.GT:
|
||||
if str, err = q.VisitGT(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.GTE:
|
||||
if str, err = q.VisitGTE(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.LT:
|
||||
if str, err = q.VisitLT(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.LTE:
|
||||
if str, err = q.VisitLTE(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.IN:
|
||||
if str, err = q.VisitIN(f); err != nil {
|
||||
return "", err
|
||||
|
@ -214,3 +279,38 @@ func (q *Query) whereFieldEqual(key string, value interface{}) string {
|
|||
query := filterField + "=$" + strconv.Itoa(position)
|
||||
return query
|
||||
}
|
||||
|
||||
func (q *Query) whereFieldNotEqual(key string, value interface{}) string {
|
||||
position := q.addParamValueAndReturnPosition(value)
|
||||
filterField := translateFieldToFilter(key)
|
||||
query := filterField + "!=$" + strconv.Itoa(position)
|
||||
return query
|
||||
}
|
||||
|
||||
func (q *Query) whereFieldGreaterThan(key string, value interface{}) string {
|
||||
position := q.addParamValueAndReturnPosition(value)
|
||||
filterField := translateFieldToFilter(key)
|
||||
query := filterField + ">$" + strconv.Itoa(position)
|
||||
return query
|
||||
}
|
||||
|
||||
func (q *Query) whereFieldGreaterThanEqual(key string, value interface{}) string {
|
||||
position := q.addParamValueAndReturnPosition(value)
|
||||
filterField := translateFieldToFilter(key)
|
||||
query := filterField + ">=$" + strconv.Itoa(position)
|
||||
return query
|
||||
}
|
||||
|
||||
func (q *Query) whereFieldLessThan(key string, value interface{}) string {
|
||||
position := q.addParamValueAndReturnPosition(value)
|
||||
filterField := translateFieldToFilter(key)
|
||||
query := filterField + "<$" + strconv.Itoa(position)
|
||||
return query
|
||||
}
|
||||
|
||||
func (q *Query) whereFieldLessThanEqual(key string, value interface{}) string {
|
||||
position := q.addParamValueAndReturnPosition(value)
|
||||
filterField := translateFieldToFilter(key)
|
||||
query := filterField + "<=$" + strconv.Itoa(position)
|
||||
return query
|
||||
}
|
||||
|
|
|
@ -49,10 +49,18 @@ func TestPostgresqlQueryBuildQuery(t *testing.T) {
|
|||
input: "../../../tests/state/query/q4.json",
|
||||
query: "SELECT key, value, xmin as etag FROM state WHERE (value->'person'->>'org'=$1 OR (value->'person'->>'org'=$2 AND (value->>'state'=$3 OR value->>'state'=$4))) ORDER BY value->>'state' DESC, value->'person'->>'name' LIMIT 2",
|
||||
},
|
||||
{
|
||||
input: "../../../tests/state/query/q4-notequal.json",
|
||||
query: "SELECT key, value, xmin as etag FROM state WHERE (value->'person'->>'org'=$1 OR (value->'person'->>'org'!=$2 AND (value->>'state'=$3 OR value->>'state'=$4))) ORDER BY value->>'state' DESC, value->'person'->>'name' LIMIT 2",
|
||||
},
|
||||
{
|
||||
input: "../../../tests/state/query/q5.json",
|
||||
query: "SELECT key, value, xmin as etag FROM state WHERE (value->'person'->>'org'=$1 AND (value->'person'->>'name'=$2 OR (value->>'state'=$3 OR value->>'state'=$4))) ORDER BY value->>'state' DESC, value->'person'->>'name' LIMIT 2",
|
||||
},
|
||||
{
|
||||
input: "../../../tests/state/query/q8.json",
|
||||
query: "SELECT key, value, xmin as etag FROM state WHERE (value->'person'->>'org'>=$1 OR (value->'person'->>'org'<$2 AND (value->>'state'=$3 OR value->>'state'=$4))) ORDER BY value->>'state' DESC, value->'person'->>'name' LIMIT 2",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
data, err := os.ReadFile(test.input)
|
||||
|
|
|
@ -80,6 +80,31 @@ type FilterEQ struct {
|
|||
Val interface{}
|
||||
}
|
||||
|
||||
type FilterNEQ struct {
|
||||
Key string
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
type FilterGT struct {
|
||||
Key string
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
type FilterGTE struct {
|
||||
Key string
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
type FilterLT struct {
|
||||
Key string
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
type FilterLTE struct {
|
||||
Key string
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
type FilterIN struct {
|
||||
Key string
|
||||
Vals []interface{}
|
||||
|
@ -100,6 +125,16 @@ To simplify the process of query translation, we leveraged [visitor design patte
|
|||
type Visitor interface {
|
||||
// returns "equal" expression
|
||||
VisitEQ(*FilterEQ) (string, error)
|
||||
// returns "not equal" expression
|
||||
VisitNEQ(*FilterNEQ) (string, error)
|
||||
// returns "greater than" expression
|
||||
VisitGT(*FilterGT) (string, error)
|
||||
// returns "greater than equal" expression
|
||||
VisitGTE(*FilterGTE) (string, error)
|
||||
// returns "less than" expression
|
||||
VisitLT(*FilterLT) (string, error)
|
||||
// returns "less than equal" expression
|
||||
VisitLTE(*FilterLTE) (string, error)
|
||||
// returns "in" expression
|
||||
VisitIN(*FilterIN) (string, error)
|
||||
// returns "and" expression
|
||||
|
@ -152,4 +187,4 @@ func (m *MyComponent) Query(req *state.QueryRequest) (*state.QueryResponse, erro
|
|||
}
|
||||
```
|
||||
|
||||
Some of the examples of State Query API implementation are [MongoDB](./mongodb/mongodb_query.go) and [CosmosDB](./azure/cosmosdb/cosmosdb_query.go) state store components.
|
||||
Some of the examples of State Query API implementation are [Redis](./redis/redis_query.go), [MongoDB](./mongodb/mongodb_query.go) and [CosmosDB](./azure/cosmosdb/cosmosdb_query.go) state store components.
|
||||
|
|
|
@ -50,6 +50,76 @@ func (q *Query) VisitEQ(f *query.EQ) (string, error) {
|
|||
return replaceKeywords("c.value."+f.Key) + " = " + name, nil
|
||||
}
|
||||
|
||||
func (q *Query) VisitNEQ(f *query.NEQ) (string, error) {
|
||||
// <key> != <val>
|
||||
val, ok := f.Val.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unsupported type of value %#v; expected string", f.Val)
|
||||
}
|
||||
name := q.setNextParameter(val)
|
||||
|
||||
return replaceKeywords("c.value."+f.Key) + " != " + name, nil
|
||||
}
|
||||
|
||||
func (q *Query) VisitGT(f *query.GT) (string, error) {
|
||||
// <key> > <val>
|
||||
var name string
|
||||
switch value := f.Val.(type) {
|
||||
case int:
|
||||
name = q.setNextParameterInt(value)
|
||||
case float64:
|
||||
name = q.setNextParameterFloat(value)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported type of value %#v; expected number", f.Val)
|
||||
}
|
||||
return replaceKeywords("c.value."+f.Key) + " > " + name, nil
|
||||
}
|
||||
|
||||
func (q *Query) VisitGTE(f *query.GTE) (string, error) {
|
||||
// <key> >= <val>
|
||||
var name string
|
||||
switch value := f.Val.(type) {
|
||||
case int:
|
||||
name = q.setNextParameterInt(value)
|
||||
case float64:
|
||||
name = q.setNextParameterFloat(value)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported type of value %#v; expected number", f.Val)
|
||||
}
|
||||
|
||||
return replaceKeywords("c.value."+f.Key) + " >= " + name, nil
|
||||
}
|
||||
|
||||
func (q *Query) VisitLT(f *query.LT) (string, error) {
|
||||
// <key> < <val>
|
||||
var name string
|
||||
switch value := f.Val.(type) {
|
||||
case int:
|
||||
name = q.setNextParameterInt(value)
|
||||
case float64:
|
||||
name = q.setNextParameterFloat(value)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported type of value %#v; expected number", f.Val)
|
||||
}
|
||||
|
||||
return replaceKeywords("c.value."+f.Key) + " < " + name, nil
|
||||
}
|
||||
|
||||
func (q *Query) VisitLTE(f *query.LTE) (string, error) {
|
||||
// <key> <= <val>
|
||||
var name string
|
||||
switch value := f.Val.(type) {
|
||||
case int:
|
||||
name = q.setNextParameterInt(value)
|
||||
case float64:
|
||||
name = q.setNextParameterFloat(value)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported type of value %#v; expected number", f.Val)
|
||||
}
|
||||
|
||||
return replaceKeywords("c.value."+f.Key) + " <= " + name, nil
|
||||
}
|
||||
|
||||
func (q *Query) VisitIN(f *query.IN) (string, error) {
|
||||
// <key> IN ( <val1>, <val2>, ... , <valN> )
|
||||
if len(f.Vals) == 0 {
|
||||
|
@ -80,6 +150,31 @@ func (q *Query) visitFilters(op string, filters []query.Filter) (string, error)
|
|||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.NEQ:
|
||||
if str, err = q.VisitNEQ(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.GT:
|
||||
if str, err = q.VisitGT(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.GTE:
|
||||
if str, err = q.VisitGTE(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.LT:
|
||||
if str, err = q.VisitLT(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.LTE:
|
||||
if str, err = q.VisitLTE(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.IN:
|
||||
if str, err = q.VisitIN(f); err != nil {
|
||||
return "", err
|
||||
|
@ -144,6 +239,20 @@ func (q *Query) setNextParameter(val string) string {
|
|||
return pname
|
||||
}
|
||||
|
||||
func (q *Query) setNextParameterInt(val int) string {
|
||||
pname := fmt.Sprintf("@__param__%d__", len(q.query.parameters))
|
||||
q.query.parameters = append(q.query.parameters, azcosmos.QueryParameter{Name: pname, Value: val})
|
||||
|
||||
return pname
|
||||
}
|
||||
|
||||
func (q *Query) setNextParameterFloat(val float64) string {
|
||||
pname := fmt.Sprintf("@__param__%d__", len(q.query.parameters))
|
||||
q.query.parameters = append(q.query.parameters, azcosmos.QueryParameter{Name: pname, Value: val})
|
||||
|
||||
return pname
|
||||
}
|
||||
|
||||
func (q *Query) execute(ctx context.Context, client *azcosmos.ContainerClient) ([]state.QueryItem, string, error) {
|
||||
opts := &azcosmos.QueryOptions{}
|
||||
|
||||
|
|
|
@ -126,6 +126,54 @@ func TestCosmosDbQuery(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "../../../tests/state/query/q4-notequal.json",
|
||||
query: InternalQuery{
|
||||
query: "SELECT * FROM c WHERE c['value']['person']['org'] = @__param__0__ OR (c['value']['person']['org'] != @__param__1__ AND c['value']['state'] IN (@__param__2__, @__param__3__)) ORDER BY c['value']['state'] DESC, c['value']['person']['name'] ASC",
|
||||
parameters: []azcosmos.QueryParameter{
|
||||
{
|
||||
Name: "@__param__0__",
|
||||
Value: "A",
|
||||
},
|
||||
{
|
||||
Name: "@__param__1__",
|
||||
Value: "B",
|
||||
},
|
||||
{
|
||||
Name: "@__param__2__",
|
||||
Value: "CA",
|
||||
},
|
||||
{
|
||||
Name: "@__param__3__",
|
||||
Value: "WA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "../../../tests/state/query/q8.json",
|
||||
query: InternalQuery{
|
||||
query: "SELECT * FROM c WHERE c['value']['person']['org'] >= @__param__0__ OR (c['value']['person']['org'] < @__param__1__ AND c['value']['state'] IN (@__param__2__, @__param__3__)) ORDER BY c['value']['state'] DESC, c['value']['person']['name'] ASC",
|
||||
parameters: []azcosmos.QueryParameter{
|
||||
{
|
||||
Name: "@__param__0__",
|
||||
Value: 123.0,
|
||||
},
|
||||
{
|
||||
Name: "@__param__1__",
|
||||
Value: 10.0,
|
||||
},
|
||||
{
|
||||
Name: "@__param__2__",
|
||||
Value: "CA",
|
||||
},
|
||||
{
|
||||
Name: "@__param__3__",
|
||||
Value: "WA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
data, err := os.ReadFile(test.input)
|
||||
|
|
|
@ -45,6 +45,56 @@ func (q *Query) VisitEQ(f *query.EQ) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitNEQ(f *query.NEQ) (string, error) {
|
||||
// { <key>: <val> }
|
||||
switch v := f.Val.(type) {
|
||||
case string:
|
||||
return fmt.Sprintf(`{ "value.%s": {"$ne": %q} }`, f.Key, v), nil
|
||||
default:
|
||||
return fmt.Sprintf(`{ "value.%s": {"$ne": %v} }`, f.Key, v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitGT(f *query.GT) (string, error) {
|
||||
// { <key>: <val> }
|
||||
switch v := f.Val.(type) {
|
||||
case string:
|
||||
return "", fmt.Errorf("unsupported type of value %s; string type not permitted", f.Val)
|
||||
default:
|
||||
return fmt.Sprintf(`{ "value.%s": {"$gt": %v} }`, f.Key, v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitGTE(f *query.GTE) (string, error) {
|
||||
// { <key>: <val> }
|
||||
switch v := f.Val.(type) {
|
||||
case string:
|
||||
return "", fmt.Errorf("unsupported type of value %s; string type not permitted", f.Val)
|
||||
default:
|
||||
return fmt.Sprintf(`{ "value.%s": {"$gte": %v} }`, f.Key, v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitLT(f *query.LT) (string, error) {
|
||||
// { <key>: <val> }
|
||||
switch v := f.Val.(type) {
|
||||
case string:
|
||||
return "", fmt.Errorf("unsupported type of value %s; string type not permitted", f.Val)
|
||||
default:
|
||||
return fmt.Sprintf(`{ "value.%s": {"$lt": %v} }`, f.Key, v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitLTE(f *query.LTE) (string, error) {
|
||||
// { <key>: <val> }
|
||||
switch v := f.Val.(type) {
|
||||
case string:
|
||||
return "", fmt.Errorf("unsupported type of value %s; string type not permitted", f.Val)
|
||||
default:
|
||||
return fmt.Sprintf(`{ "value.%s": {"$lte": %v} }`, f.Key, v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitIN(f *query.IN) (string, error) {
|
||||
// { $in: [ <val1>, <val2>, ... , <valN> ] }
|
||||
if len(f.Vals) == 0 {
|
||||
|
@ -81,6 +131,31 @@ func (q *Query) visitFilters(op string, filters []query.Filter) (string, error)
|
|||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.NEQ:
|
||||
if str, err = q.VisitNEQ(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.GT:
|
||||
if str, err = q.VisitGT(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.GTE:
|
||||
if str, err = q.VisitGTE(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.LT:
|
||||
if str, err = q.VisitLT(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.LTE:
|
||||
if str, err = q.VisitLTE(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, str)
|
||||
case *query.IN:
|
||||
if str, err = q.VisitIN(f); err != nil {
|
||||
return "", err
|
||||
|
|
|
@ -49,6 +49,14 @@ func TestMongoQuery(t *testing.T) {
|
|||
input: "../../tests/state/query/q6.json",
|
||||
query: `{ "$or": [ { "value.person.id": 123 }, { "$and": [ { "value.person.org": "B" }, { "value.person.id": { "$in": [ 567, 890 ] } } ] } ] }`,
|
||||
},
|
||||
{
|
||||
input: "../../tests/state/query/q6-notequal.json",
|
||||
query: `{ "$or": [ { "value.person.id": 123 }, { "$and": [ { "value.person.org": {"$ne": "B"} }, { "value.person.id": { "$in": [ 567, 890 ] } } ] } ] }`,
|
||||
},
|
||||
{
|
||||
input: "../../tests/state/query/q7.json",
|
||||
query: `{ "$or": [ { "value.person.id": {"$lt": 123} }, { "$and": [ { "value.person.org": {"$gte": 2} }, { "value.person.id": { "$in": [ 567, 890 ] } } ] } ] }`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
data, err := os.ReadFile(test.input)
|
||||
|
|
|
@ -36,6 +36,31 @@ func ParseFilter(obj interface{}) (Filter, error) {
|
|||
f := &EQ{}
|
||||
err := f.Parse(v)
|
||||
|
||||
return f, err
|
||||
case "NEQ":
|
||||
f := &NEQ{}
|
||||
err := f.Parse(v)
|
||||
|
||||
return f, err
|
||||
case "GT":
|
||||
f := >{}
|
||||
err := f.Parse(v)
|
||||
|
||||
return f, err
|
||||
case "GTE":
|
||||
f := >E{}
|
||||
err := f.Parse(v)
|
||||
|
||||
return f, err
|
||||
case "LT":
|
||||
f := <{}
|
||||
err := f.Parse(v)
|
||||
|
||||
return f, err
|
||||
case "LTE":
|
||||
f := <E{}
|
||||
err := f.Parse(v)
|
||||
|
||||
return f, err
|
||||
case "IN":
|
||||
f := &IN{}
|
||||
|
@ -81,6 +106,111 @@ func (f *EQ) Parse(obj interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type NEQ struct {
|
||||
Key string
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
func (f *NEQ) Parse(obj interface{}) error {
|
||||
m, ok := obj.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("NEQ filter must be a map")
|
||||
}
|
||||
if len(m) != 1 {
|
||||
return fmt.Errorf("NEQ filter must contain a single key/value pair")
|
||||
}
|
||||
for k, v := range m {
|
||||
f.Key = k
|
||||
f.Val = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type GT struct {
|
||||
Key string
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
func (f *GT) Parse(obj interface{}) error {
|
||||
m, ok := obj.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("GT filter must be a map")
|
||||
}
|
||||
if len(m) != 1 {
|
||||
return fmt.Errorf("GT filter must contain a single key/value pair")
|
||||
}
|
||||
for k, v := range m {
|
||||
f.Key = k
|
||||
f.Val = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type GTE struct {
|
||||
Key string
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
func (f *GTE) Parse(obj interface{}) error {
|
||||
m, ok := obj.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("GTE filter must be a map")
|
||||
}
|
||||
if len(m) != 1 {
|
||||
return fmt.Errorf("GTE filter must contain a single key/value pair")
|
||||
}
|
||||
for k, v := range m {
|
||||
f.Key = k
|
||||
f.Val = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type LT struct {
|
||||
Key string
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
func (f *LT) Parse(obj interface{}) error {
|
||||
m, ok := obj.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("LT filter must be a map")
|
||||
}
|
||||
if len(m) != 1 {
|
||||
return fmt.Errorf("LT filter must contain a single key/value pair")
|
||||
}
|
||||
for k, v := range m {
|
||||
f.Key = k
|
||||
f.Val = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type LTE struct {
|
||||
Key string
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
func (f *LTE) Parse(obj interface{}) error {
|
||||
m, ok := obj.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("LTE filter must be a map")
|
||||
}
|
||||
if len(m) != 1 {
|
||||
return fmt.Errorf("LTE filter must contain a single key/value pair")
|
||||
}
|
||||
for k, v := range m {
|
||||
f.Key = k
|
||||
f.Val = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type IN struct {
|
||||
Key string
|
||||
Vals []interface{}
|
||||
|
|
|
@ -53,6 +53,16 @@ type Query struct {
|
|||
type Visitor interface {
|
||||
// returns "equal" expression
|
||||
VisitEQ(*EQ) (string, error)
|
||||
// returns "not equal" expression
|
||||
VisitNEQ(*NEQ) (string, error)
|
||||
// returns "greater than" expression
|
||||
VisitGT(*GT) (string, error)
|
||||
// returns "greater than equal" expression
|
||||
VisitGTE(*GTE) (string, error)
|
||||
// returns "less than" expression
|
||||
VisitLT(*LT) (string, error)
|
||||
// returns "less than equal" expression
|
||||
VisitLTE(*LTE) (string, error)
|
||||
// returns "in" expression
|
||||
VisitIN(*IN) (string, error)
|
||||
// returns "and" expression
|
||||
|
@ -89,6 +99,16 @@ func (h *Builder) buildFilter(filter Filter) (string, error) {
|
|||
switch f := filter.(type) {
|
||||
case *EQ:
|
||||
return h.visitor.VisitEQ(f)
|
||||
case *NEQ:
|
||||
return h.visitor.VisitNEQ(f)
|
||||
case *GT:
|
||||
return h.visitor.VisitGT(f)
|
||||
case *GTE:
|
||||
return h.visitor.VisitGTE(f)
|
||||
case *LT:
|
||||
return h.visitor.VisitLT(f)
|
||||
case *LTE:
|
||||
return h.visitor.VisitLTE(f)
|
||||
case *IN:
|
||||
return h.visitor.VisitIN(f)
|
||||
case *OR:
|
||||
|
|
|
@ -66,6 +66,81 @@ func (q *Query) VisitEQ(f *query.EQ) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitNEQ(f *query.NEQ) (string, error) {
|
||||
// string: @<key>:(<val>)
|
||||
// numeric: @<key>:[<val> <val>]
|
||||
alias, err := q.getAlias(f.Key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch v := f.Val.(type) {
|
||||
case string:
|
||||
return fmt.Sprintf("@%s:(%s)", alias, v), nil
|
||||
default:
|
||||
return fmt.Sprintf("@%s:[%v %v]", alias, v, v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitGT(f *query.GT) (string, error) {
|
||||
// numeric: @<key>:[(<val> +inf]
|
||||
alias, err := q.getAlias(f.Key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch v := f.Val.(type) {
|
||||
case string:
|
||||
return "", fmt.Errorf("unsupported type of value %s; string type not permitted", f.Val)
|
||||
default:
|
||||
return fmt.Sprintf("@%s:[(%v +inf]", alias, v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitGTE(f *query.GTE) (string, error) {
|
||||
// numeric: @<key>:[<val> +inf]
|
||||
alias, err := q.getAlias(f.Key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch v := f.Val.(type) {
|
||||
case string:
|
||||
return "", fmt.Errorf("unsupported type of value %s; string type not permitted", f.Val)
|
||||
default:
|
||||
return fmt.Sprintf("@%s:[%v +inf]", alias, v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitLT(f *query.LT) (string, error) {
|
||||
// numeric: @<key>:[-inf <val>)]
|
||||
alias, err := q.getAlias(f.Key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch v := f.Val.(type) {
|
||||
case string:
|
||||
return "", fmt.Errorf("unsupported type of value %s; string type not permitted", f.Val)
|
||||
default:
|
||||
return fmt.Sprintf("@%s:[-inf (%v]", alias, v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitLTE(f *query.LTE) (string, error) {
|
||||
// numeric: @<key>:[-inf <val>]
|
||||
alias, err := q.getAlias(f.Key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch v := f.Val.(type) {
|
||||
case string:
|
||||
return "", fmt.Errorf("unsupported type of value %s; string type not permitted", f.Val)
|
||||
default:
|
||||
return fmt.Sprintf("@%s:[-inf %v]", alias, v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) VisitIN(f *query.IN) (string, error) {
|
||||
// string: @<key>:(<val1>|<val2>...)
|
||||
// numeric: replace with OR
|
||||
|
@ -116,6 +191,31 @@ func (q *Query) visitFilters(op string, filters []query.Filter) (string, error)
|
|||
return "", err
|
||||
}
|
||||
arr = append(arr, fmt.Sprintf("(%s)", str))
|
||||
case *query.NEQ:
|
||||
if str, err = q.VisitNEQ(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, fmt.Sprintf("-(%s)", str))
|
||||
case *query.GT:
|
||||
if str, err = q.VisitGT(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, fmt.Sprintf("(%s)", str))
|
||||
case *query.GTE:
|
||||
if str, err = q.VisitGTE(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, fmt.Sprintf("(%s)", str))
|
||||
case *query.LT:
|
||||
if str, err = q.VisitLT(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, fmt.Sprintf("(%s)", str))
|
||||
case *query.LTE:
|
||||
if str, err = q.VisitLTE(f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
arr = append(arr, fmt.Sprintf("(%s)", str))
|
||||
case *query.IN:
|
||||
if str, err = q.VisitIN(f); err != nil {
|
||||
return "", err
|
||||
|
|
|
@ -46,6 +46,14 @@ func TestMongoQuery(t *testing.T) {
|
|||
input: "../../tests/state/query/q6.json",
|
||||
query: []interface{}{"((@id:[123 123])|((@org:(B)) (((@id:[567 567])|(@id:[890 890])))))", "SORTBY", "id", "LIMIT", "0", "2"},
|
||||
},
|
||||
{
|
||||
input: "../../tests/state/query/q6-notequal.json",
|
||||
query: []interface{}{"((@id:[123 123])|(-(@org:(B)) (((@id:[567 567])|(@id:[890 890])))))", "SORTBY", "id", "LIMIT", "0", "2"},
|
||||
},
|
||||
{
|
||||
input: "../../tests/state/query/q7.json",
|
||||
query: []interface{}{"((@id:[-inf (123])|((@org:[2 +inf]) (((@id:[567 567])|(@id:[890 890])))))", "SORTBY", "id", "LIMIT", "0", "2"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
data, err := os.ReadFile(test.input)
|
||||
|
|
|
@ -18,6 +18,14 @@ spec:
|
|||
{
|
||||
"key": "message",
|
||||
"type": "TEXT"
|
||||
},
|
||||
{
|
||||
"key": "product.value",
|
||||
"type": "NUMERIC"
|
||||
},
|
||||
{
|
||||
"key": "status",
|
||||
"type": "TEXT"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -40,6 +40,13 @@ type ValueType struct {
|
|||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type StructType struct {
|
||||
Product struct {
|
||||
Value int `json:"value"`
|
||||
} `json:"product"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type intValueType struct {
|
||||
Message int32 `json:"message"`
|
||||
}
|
||||
|
@ -119,6 +126,20 @@ func ConformanceTests(t *testing.T, props map[string]string, statestore state.St
|
|||
value: ValueType{Message: fmt.Sprintf("test%s", key)},
|
||||
contentType: contenttype.JSONContentType,
|
||||
},
|
||||
{
|
||||
key: fmt.Sprintf("%s-struct-operations", key),
|
||||
value: StructType{Product: struct {
|
||||
Value int `json:"value"`
|
||||
}{Value: 15}, Status: "ACTIVE"},
|
||||
contentType: contenttype.JSONContentType,
|
||||
},
|
||||
{
|
||||
key: fmt.Sprintf("%s-struct-operations-inactive", key),
|
||||
value: StructType{Product: struct {
|
||||
Value int `json:"value"`
|
||||
}{Value: 12}, Status: "INACTIVE"},
|
||||
contentType: contenttype.JSONContentType,
|
||||
},
|
||||
{
|
||||
key: fmt.Sprintf("%s-struct-with-int", key),
|
||||
value: intValueType{Message: 42},
|
||||
|
@ -235,6 +256,67 @@ func ConformanceTests(t *testing.T, props map[string]string, statestore state.St
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
query: `
|
||||
{
|
||||
"filter": {
|
||||
"AND": [
|
||||
{
|
||||
"GTE": {"product.value": 10}
|
||||
},
|
||||
{
|
||||
"LT": {"product.value": 20}
|
||||
},
|
||||
{
|
||||
"NEQ": {"status": "INACTIVE"}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`,
|
||||
results: []state.QueryItem{
|
||||
{
|
||||
Key: fmt.Sprintf("%s-struct-operations", key),
|
||||
Data: []byte(fmt.Sprintf(`{"product":{"value":15}, "status":"ACTIVE"}`)),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
query: `
|
||||
{
|
||||
"filter": {
|
||||
"OR": [
|
||||
{
|
||||
"AND": [
|
||||
{
|
||||
"GT": {"product.value": 11.1}
|
||||
},
|
||||
{
|
||||
"EQ": {"status": "INACTIVE"}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"AND": [
|
||||
{
|
||||
"LTE": {"product.value": 0.5}
|
||||
},
|
||||
{
|
||||
"EQ": {"status": "ACTIVE"}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`,
|
||||
results: []state.QueryItem{
|
||||
{
|
||||
Key: fmt.Sprintf("%s-struct-operations-inactive", key),
|
||||
Data: []byte(fmt.Sprintf(`{"product":{"value":12}, "status":"INACTIVE"}`)),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("init", func(t *testing.T) {
|
||||
|
@ -312,6 +394,7 @@ func ConformanceTests(t *testing.T, props map[string]string, statestore state.St
|
|||
metadata.ContentType: contenttype.JSONContentType,
|
||||
metadata.QueryIndexName: "qIndx",
|
||||
}
|
||||
|
||||
resp, err := querier.Query(context.Background(), &req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, len(scenario.results), len(resp.Results))
|
||||
|
@ -1241,6 +1324,12 @@ func assertDataEquals(t *testing.T, expect any, actual []byte) {
|
|||
assert.Failf(t, "unmarshal error", "error: %v, json: %s", err, string(actual))
|
||||
}
|
||||
assert.Equal(t, expect, v)
|
||||
case StructType:
|
||||
// Custom type requires case mapping
|
||||
if err := json.Unmarshal(actual, &v); err != nil {
|
||||
assert.Failf(t, "unmarshal error", "error: %v, json: %s", err, string(actual))
|
||||
}
|
||||
assert.Equal(t, expect, v)
|
||||
case int:
|
||||
// json.Unmarshal to float64 by default, case mapping to int coerces to int type
|
||||
if err := json.Unmarshal(actual, &v); err != nil {
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"filter": {
|
||||
"OR": [
|
||||
{
|
||||
"EQ": {
|
||||
"person.org": "A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"AND": [
|
||||
{
|
||||
"NEQ": {
|
||||
"person.org": "B"
|
||||
}
|
||||
},
|
||||
{
|
||||
"IN": {
|
||||
"state": ["CA", "WA"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"sort": [
|
||||
{
|
||||
"key": "state",
|
||||
"order": "DESC"
|
||||
},
|
||||
{
|
||||
"key": "person.name"
|
||||
}
|
||||
],
|
||||
"page": {
|
||||
"limit": 2
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"filter": {
|
||||
"OR": [
|
||||
{
|
||||
"EQ": {
|
||||
"person.id": 123
|
||||
}
|
||||
},
|
||||
{
|
||||
"AND": [
|
||||
{
|
||||
"NEQ": {
|
||||
"person.org": "B"
|
||||
}
|
||||
},
|
||||
{
|
||||
"IN": {
|
||||
"person.id": [567, 890]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"sort": [
|
||||
{
|
||||
"key": "person.id"
|
||||
}
|
||||
],
|
||||
"page": {
|
||||
"limit": 2
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"filter": {
|
||||
"OR": [
|
||||
{
|
||||
"LT": {
|
||||
"person.id": 123
|
||||
}
|
||||
},
|
||||
{
|
||||
"AND": [
|
||||
{
|
||||
"GTE": {
|
||||
"person.org": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"IN": {
|
||||
"person.id": [
|
||||
567,
|
||||
890
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"sort": [
|
||||
{
|
||||
"key": "person.id"
|
||||
}
|
||||
],
|
||||
"page": {
|
||||
"limit": 2
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"filter": {
|
||||
"OR": [
|
||||
{
|
||||
"GTE": {
|
||||
"person.org": 123
|
||||
}
|
||||
},
|
||||
{
|
||||
"AND": [
|
||||
{
|
||||
"LT": {
|
||||
"person.org": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"IN": {
|
||||
"state": [
|
||||
"CA",
|
||||
"WA"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"sort": [
|
||||
{
|
||||
"key": "state",
|
||||
"order": "DESC"
|
||||
},
|
||||
{
|
||||
"key": "person.name"
|
||||
}
|
||||
],
|
||||
"page": {
|
||||
"limit": 2
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue