Skip to content

Commit

Permalink
Change unset vars to default (#2906) (#2909)
Browse files Browse the repository at this point in the history
* set unassigned vars to default type when evaluated

* add test for unmatched var evals

* unset vars default to zero float in aggregation results

* missing needvars mean unset vars, error otherwise

* removed test call which had conflict
  • Loading branch information
srfrog authored and danielmai committed Jan 17, 2019
1 parent 438278a commit 266e75f
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 2 deletions.
7 changes: 6 additions & 1 deletion query/outputnode.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,12 @@ func (fj *fastJsonNode) addAggregations(sg *SubGraph) error {
for _, child := range sg.Children {
aggVal, ok := child.Params.uidToVal[0]
if !ok {
return x.Errorf("Only aggregated variables allowed within empty block.")
if len(child.Params.NeedsVar) == 0 {
return x.Errorf("Only aggregated variables allowed within empty block.")
}
// the aggregation didn't happen, most likely was called with unset vars.
// See: query.go:fillVars
aggVal = types.Val{Tid: types.FloatID, Value: float64(0)}
}
if child.Params.Normalize && child.Params.Alias == "" {
continue
Expand Down
2 changes: 1 addition & 1 deletion query/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -1608,7 +1608,7 @@ func (sg *SubGraph) fillVars(mp map[string]varValue) error {
// Provide a default value for valueVarAggregation() to eval val().
// This is a noop for aggregation funcs that would fail.
// The zero aggs won't show because there are no uids matched.
mp[v.Name].Vals[0] = types.Val{Tid: types.FloatID, Value: 0.0}
mp[v.Name].Vals[0] = types.Val{}
sg.Params.uidToVal = mp[v.Name].Vals
}
}
Expand Down
256 changes: 256 additions & 0 deletions systest/queries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"testing"

Expand All @@ -46,13 +47,268 @@ func TestQuery(t *testing.T) {
t.Run("schema predicate names", wrap(SchemaQueryTestPredicate1))
t.Run("schema specific predicate fields", wrap(SchemaQueryTestPredicate2))
t.Run("schema specific predicate field", wrap(SchemaQueryTestPredicate3))
t.Run("multiple block eval", wrap(MultipleBlockEval))
t.Run("unmatched var assignment eval", wrap(UnmatchedVarEval))
t.Run("cleanup", wrap(SchemaQueryCleanup))
}

func SchemaQueryCleanup(t *testing.T, c *dgo.Dgraph) {
require.NoError(t, c.Alter(context.Background(), &api.Operation{DropAll: true}))
}

func MultipleBlockEval(t *testing.T, c *dgo.Dgraph) {
ctx := context.Background()

require.NoError(t, c.Alter(ctx, &api.Operation{
Schema: `
entity: string @index(exact) .
stock: uid @reverse .
`,
}))

txn := c.NewTxn()
_, err := txn.Mutate(ctx, &api.Mutation{
SetNquads: []byte(`
_:e1 <entity> "672E1D63-4921-420C-90A8-5B39DD77B89F" .
_:e1 <entity.type> "chair" .
_:e1 <entity.price> "2999.99"^^<xs:float> .
_:e2 <entity> "03B9CB73-7424-4BE5-AE39-97CC4D2F3A21" .
_:e2 <entity.type> "sofa" .
_:e2 <entity.price> "899.0"^^<xs:float> .
_:e1 <combo> _:e2 .
_:e2 <combo> _:e1 .
_:e3 <entity> "BDFD64A3-5CA8-41F0-98D6-E65A9DAE092D" .
_:e3 <entity.type> "desk" .
_:e3 <entity.price> "578.99"^^<xs:float> .
_:e4 <entity> "AE1D1A85-9C26-4A1D-9B0B-00A8BBCFDDA0" .
_:e4 <entity.type> "lamp" .
_:e4 <entity.price> "199.99"^^<xs:float> .
_:e3 <combo> _:e4 .
_:e4 <combo> _:e3 .
_:e5 <entity> "9021E504-65B7-4939-8C02-F73CFD5635F6" .
_:e5 <entity.type> "table" .
_:e5 <entity.price> "1899.98"^^<xs:float> .
# table has no combo
_:s1 <stock> _:e1 .
_:s1 <stock.in> "100"^^<xs:int> .
_:s1 <stock.note> "Over-stocked" .
_:e1 <stock> _:s1 .
_:s2 <stock> _:e2 .
_:s2 <stock.in> "20"^^<xs:int> .
_:s2 <stock.note> "Running low, order more" .
_:e2 <stock> _:s2 .
_:s3 <stock> _:e3 .
_:s3 <stock.in> "25"^^<xs:int> .
_:s3 <stock.note> "Delicate needs insurance" .
_:e3 <stock> _:s3 .
_:s4 <stock> _:e4 .
_:s4 <stock.out> "true"^^<xs:boolean> .
_:s4 <stock.note> "Out of stock" .
_:e4 <stock> _:s4 .
_:s5 <stock> _:e5 .
_:s5 <stock.out> "true"^^<xs:boolean> .
_:s5 <stock.note> "Out of stock" .
_:e5 <stock> _:s5 .
`),
})
require.NoError(t, err)
require.NoError(t, txn.Commit(ctx))

tests := []struct {
in string
out string
}{
{in: "672E1D63-4921-420C-90A8-5B39DD77B89F",
out: `{"q": [{
"notes": [{
"stock.note": "Over-stocked"
}],
"sku": "672E1D63-4921-420C-90A8-5B39DD77B89F",
"type": "chair",
"combos": 1
}]}`},
{in: "03B9CB73-7424-4BE5-AE39-97CC4D2F3A21",
out: `{"q": [{
"notes": [{
"stock.note": "Running low, order more"
}],
"sku": "03B9CB73-7424-4BE5-AE39-97CC4D2F3A21",
"type": "sofa",
"combos": 1
}]}`},
{in: "BDFD64A3-5CA8-41F0-98D6-E65A9DAE092D",
out: `{"q": [{
"notes": [{
"stock.note": "Delicate needs insurance"
}],
"sku": "BDFD64A3-5CA8-41F0-98D6-E65A9DAE092D",
"type": "desk",
"combos": 1
}]}`},
{in: "AE1D1A85-9C26-4A1D-9B0B-00A8BBCFDDA0",
out: `{"q": [{
"combos": 1,
"notes": [{
"stock.note": "Out of stock"
}],
"sku": "AE1D1A85-9C26-4A1D-9B0B-00A8BBCFDDA0",
"type": "lamp"
}]}`},
{in: "9021E504-65B7-4939-8C02-F73CFD5635F6",
out: `{"q":[]}`},
}

queryFmt := `
{
filter_uids as var(func: eq(entity, "%s"))
@filter(has(entity.type) and not(has(stock.out)) and (has(combo)))
entity_uids as var (func: uid(filter_uids)) @filter()
var(func: uid(entity_uids)) {
stock_uid as stock
}
var(func: uid(entity_uids)) {
stock {
available as stock @filter(has(entity.price) and not(has(stock.out)))
}
}
var(func: uid(available)) @cascade {
combo {
cnt_combos as math(1)
combo {
~stock {
~stock @filter(has(combo))
}
}
}
available_combos as sum(val(cnt_combos))
}
q(func: uid(available)) {
sku: entity
type: entity.type
notes: ~stock @filter(uid(stock_uid)) {
stock.note
}
combos: val(available_combos)
}
}`

txn = c.NewTxn()
for _, tc := range tests {
resp, err := txn.Query(ctx, fmt.Sprintf(queryFmt, tc.in))
require.NoError(t, err)
CompareJSON(t, tc.out, string(resp.Json))
}
}

func UnmatchedVarEval(t *testing.T, c *dgo.Dgraph) {
ctx := context.Background()

require.NoError(t, c.Alter(ctx, &api.Operation{
Schema: `
item: string @index(hash) .
style.type: string .
style.name: string .
style.cool: bool .
`,
}))

txn := c.NewTxn()
_, err := txn.Mutate(ctx, &api.Mutation{
SetNquads: []byte(`
_:a <item> "chair" .
_:a <style.name> "Modern leather chair" .
_:a <style.cool> "true" .
_:b <item> "chair" .
_:b <style.name> "Broken beanbag" .
_:b <style.cool> "false"^^<xs:boolean> .
_:c <item> "sofa" .
_:c <style.name> "U-shape sectional" .
_:c <style.cool> "true"^^<xs:boolean> .
_:d <item> "table" .
_:d <style.name> "Glass top marble table" .
_:d <style.cool> "true"^^<xs:boolean> .
`),
})
require.NoError(t, err)
require.NoError(t, txn.Commit(ctx))

tests := []struct {
in string
out string
}{
{
in: `
{
items as var(func: eq(item, "chair")) @filter(has(style.name)) @cascade {
is_cool as style.cool
}
q(func: eq(item, "chair")) @filter(eq(val(is_cool), false) AND uid(items)) {
item
style.name
style.cool
is_cool
}
}`,
out: `
{
"q": [
{
"item": "chair",
"style.cool": false,
"style.name": "Broken beanbag"
}
]
}`,
},
{
// filtering by dummy (no such pred) reduces to empty set.
// is_cool would be assigned as default type to aid in filter eval.
// @filter(eq(val(is_cool), false) would effectively evaluate: "" eq "false"
in: `
{
items as var(func: eq(item, "chair")) @filter(has(dummy)) @cascade {
is_cool as style.cool
}
q(func: eq(item, "chair")) @filter(eq(val(is_cool), false) AND uid(items)) {
item
style.name
style.cool
is_cool
}
}`,
out: `{"q": []}`,
},
}

txn = c.NewTxn()
for _, tc := range tests {
resp, err := txn.Query(ctx, tc.in)
require.NoError(t, err)
CompareJSON(t, tc.out, string(resp.Json))
}

}

func SchemaQueryTest(t *testing.T, c *dgo.Dgraph) {
ctx := context.Background()

Expand Down

0 comments on commit 266e75f

Please sign in to comment.