Skip to content

Commit

Permalink
Merge pull request #66 from raidancampbell/master
Browse files Browse the repository at this point in the history
Add option for 'or' operations to short circuit
  • Loading branch information
jsccast committed Nov 6, 2020
2 parents 6fed90a + a71823d commit ad9830d
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 1 deletion.
19 changes: 19 additions & 0 deletions core/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ func AndQueryFromMap(ctx *Context, m map[string]interface{}) (Query, bool, error

type OrQuery struct {
Disjuncts []Query
ShortCircuit bool
}

func (q OrQuery) MarshalJSON() ([]byte, error) {
Expand All @@ -628,6 +629,9 @@ func (q OrQuery) MarshalJSON() ([]byte, error) {
return nil, err
}
buf.Write(js)
if q.ShortCircuit {
buf.WriteString(`,"shortCircuit": true`)
}
buf.WriteString(`}`)
return buf.Bytes(), nil
}
Expand All @@ -640,6 +644,9 @@ func (o OrQuery) Exec(ctx *Context, loc *Location, qc QueryContext, qr QueryResu
return nil, err
}
acc.Bss = append(acc.Bss, more.Bss...)
if o.ShortCircuit && len(more.Bss) > 0 {
break
}
}
return &acc, nil
}
Expand Down Expand Up @@ -669,6 +676,18 @@ func OrQueryFromMap(ctx *Context, m map[string]interface{}) (Query, bool, error)
}
q.Disjuncts = qs

for _, candidate := range []string{"shortCircuit", "ShortCircuit", "short_circuit", "shortcircuit"} {
sc, ok := m[candidate]
if ok {
scBool, ok := sc.(bool)
if ok {
q.ShortCircuit = scBool
break
}
return nil, false, fmt.Errorf("field %s: %v isn't a bool", candidate, sc)
}
}

return q, true, nil
}

Expand Down
34 changes: 33 additions & 1 deletion core/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,14 @@ func TestQueryOr(t *testing.T) {
assert.NoError(t, err)
}

func TestQueryOrShortCircuit(t *testing.T) {
err := doQueryTest(t,
[]string{`{"likes":"chips"}`, `{"likes":"tacos"}`, `{"drinks":"beer"}`},
`{"shortCircuit": true, "or":[{"pattern": {"likes":"?likes"}},{"pattern": {"drinks":"?drinks"}}]}`,
`[{"?likes":"tacos"},{"?likes":"chips"}]`)
assert.NoError(t, err)
}

func TestQueryAnd(t *testing.T) {
err := doQueryTest(t,
[]string{`{"likes":"rum"}`, `{"likes":"tacos"}`, `{"drinks":"rum"}`},
Expand Down Expand Up @@ -308,6 +316,13 @@ func BenchmarkQueryOr(b *testing.B) {
`[{"?likes":"tacos"},{"?likes":"tacos"},{"?drinks":"beer"}]`)
}

func BenchmarkQueryOrShortCircuit(b *testing.B) {
doQueryBenchmark(b,
[]string{`{"likes":"chips"}`, `{"likes":"tacos"}`, `{"drinks":"beer"}`},
`{"shortCircuit": true, "or":[{"pattern": {"likes":"?likes"}},{"pattern": {"drinks":"?drinks"}}]}`,
`[{"?likes":"tacos"},{"?likes":"chips"}]`)
}

func BenchmarkQueryAnd(b *testing.B) {
doQueryBenchmark(b,
[]string{`{"likes":"rum"}`, `{"likes":"tacos"}`, `{"drinks":"rum"}`},
Expand Down Expand Up @@ -462,10 +477,27 @@ func TestOrQueryFromMapBad2(t *testing.T) {
}
}

func TestOrQueryFromMapGood(t *testing.T) {
func TestOrQueryFromMapBad3(t *testing.T) {
m := map[string]interface{}{"or": []interface{}{}, "shortCircuit": "malformed"}
_, _, err := OrQueryFromMap(nil, m)
if err == nil {
t.Fatal("should have reported an error")
}
}


func TestOrQueryFromMapGood1(t *testing.T) {
m := map[string]interface{}{"or": []interface{}{}}
_, _, err := OrQueryFromMap(nil, m)
if err != nil {
t.Fatal("shouldn't have reported an error")
}
}

func TestOrQueryFromMapGood2(t *testing.T) {
m := map[string]interface{}{"or": []interface{}{}, "shortCircuit": false}
_, _, err := OrQueryFromMap(nil, m)
if err != nil {
t.Fatal("shouldn't have reported an error")
}
}
12 changes: 12 additions & 0 deletions doc/Manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,18 @@ Example query:
query. An `and` or `or` value should be an array of subqueries. A
`not` value is a single subquery.

The `or` query supports an optional `shortCircuit` flag. By default
`or` queries are exhaustive: all subqueries will be executed. When
`shortCircuit` is set, the query terminates after the first subquery
returns bindings. For example, the below query would terminate if
the `?likes` pattern produces bindings.

```Javascript
{"or": [{"pattern":{"likes":"?likes"}},
{"pattern":{"drinks":"?drinks"}}],
"shortCircuit":true}
```

A `code` value is string of Javascript or an array of strings of
Javascript. When executed, the Javascript environment includes
bindings for all variables that are bound at that point in the query
Expand Down
4 changes: 4 additions & 0 deletions doc/rule-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@
"description": "Or operation",
"type": "object",
"properties": {
"shortCircuit": {
"description": "whether the or operation should stop after the first query that successfully returns bindings",
"type": "boolean"
},
"or": {
"description": "List of queries",
"type": "array",
Expand Down

0 comments on commit ad9830d

Please sign in to comment.