diff --git a/topdown/topdown.go b/topdown/topdown.go index c81d75738a..dcb32ea231 100644 --- a/topdown/topdown.go +++ b/topdown/topdown.go @@ -159,6 +159,12 @@ const ( // UnboundGlobalErr indicates a global variable without a binding was // encountered during evaluation. UnboundGlobalErr = iota + + // ConflictErr indicates multiple (conflicting) values were produced + // while generating a virtual document. E.g., given two rules that share + // the same name: p = false :- true, p = true :- true, a query "p" would + // evaluate p to "true" and "false". + ConflictErr = iota ) func (e *Error) Error() string { @@ -173,13 +179,20 @@ func IsUnboundGlobal(e error) bool { return false } -func unboundGlobalVar(r ast.Ref) error { +func unboundGlobalVarErr(r ast.Ref) error { return &Error{ Code: UnboundGlobalErr, Message: fmt.Sprintf("unbound variable %v: %v", r[0], r), } } +func conflictErr(query interface{}, kind string, rule *ast.Rule) error { + return &Error{ + Code: ConflictErr, + Message: fmt.Sprintf("multiple values for %v: rules must produce exactly one value for %v: check rule definition(s): %v", query, kind, rule.Name), + } +} + // Iterator is the interface for processing contexts. type Iterator func(*Context) error @@ -457,7 +470,7 @@ func evalRef(ctx *Context, ref ast.Ref, iter Iterator) error { if !ref[0].Equal(ast.DefaultRootDocument) { v := ctx.Binding(ref[0].Value) if v == nil { - return unboundGlobalVar(ref) + return unboundGlobalVarErr(ref) } return evalRefRuleResult(ctx, ref, ref[1:], v, iter) } @@ -615,7 +628,7 @@ func evalRefRuleCompleteDoc(ctx *Context, ref ast.Ref, suffix ast.Ref, rules []* if isTrue && result == nil { result = rule.Value.Value } else if isTrue && result != nil { - return fmt.Errorf("multiple values for %v: incremental definitions must produce exactly one value for complete documents: check rule definitions: %v", ref, rule.Name) + return conflictErr(ref, "complete documents", rule) } } @@ -690,7 +703,7 @@ func evalRefRulePartialObjectDocFull(ctx *Context, ref ast.Ref, rules []*ast.Rul err := Eval(child, func(child *Context) error { key := child.Binding(rule.Key.Value) if _, ok := keys.Get(key); ok { - return fmt.Errorf("multiple values for %v: incremental definitions must produce exactly one value for each key of an object document: check rule definitions: %v", ref, rule.Name) + return conflictErr(ref, "object document keys", rule) } keys.Put(key, ast.Null{}) value := child.Binding(rule.Value.Value) @@ -1236,33 +1249,42 @@ func plugValue(v ast.Value, ctx *Context) ast.Value { func topDownQueryCompleteDoc(params *QueryParams, rules []*ast.Rule) (interface{}, error) { - if len(rules) > 1 { - return nil, fmt.Errorf("multiple conflicting rules: %v", rules[0].Name) + var result ast.Value + var resultContext *Context + + for _, rule := range rules { + ctx := &Context{ + Query: rule.Body, + Globals: params.Globals, + Locals: storage.NewBindings(), + DataStore: params.DataStore, + Tracer: params.Tracer, + } + + isTrue := false + + err := Eval(ctx, func(ctx *Context) error { + isTrue = true + return nil + }) + + if err != nil { + return nil, err + } + + if isTrue && result == nil { + result = rule.Value.Value + resultContext = ctx + } else if isTrue && result != nil { + return nil, conflictErr(params.Path, "complete documents", rule) + } } - rule := rules[0] - - ctx := &Context{ - Query: rule.Body, - Globals: params.Globals, - Locals: storage.NewBindings(), - DataStore: params.DataStore, - Tracer: params.Tracer, - } - - isTrue := false - err := Eval(ctx, func(ctx *Context) error { - isTrue = true - return nil - }) - if err != nil { - return nil, err - } - if !isTrue { + if result == nil { return Undefined{}, nil } - return ValueToInterface(rule.Value.Value, ctx) + return ValueToInterface(result, resultContext) } func topDownQueryPartialObjectDoc(params *QueryParams, rules []*ast.Rule) (interface{}, error) { diff --git a/topdown/topdown_test.go b/topdown/topdown_test.go index 2eff5af2fb..0bd685027b 100644 --- a/topdown/topdown_test.go +++ b/topdown/topdown_test.go @@ -484,10 +484,10 @@ func TestTopDownVirtualDocs(t *testing.T) { // no dereferencing {"no suffix: complete", []string{"p = true :- q", "q = true :- true"}, "true"}, - {"no suffix: complete incr (error)", []string{"p = true :- q", "q = false :- true", "q = true :- true"}, fmt.Errorf("multiple values for data.q: incremental definitions must produce exactly one value for complete documents: check rule definitions: q")}, + {"no suffix: complete incr (error)", []string{"p = true :- q", "q = false :- true", "q = true :- true"}, fmt.Errorf("evaluation error (code: 2): multiple values for data.q: rules must produce exactly one value for complete documents: check rule definition(s): q")}, {"no suffix: complete incr", []string{"p = true :- not q", "q = true :- false", "q = false :- true"}, "true"}, {"no suffix: object", []string{"p[x] = y :- q = o, o[x] = y", "q[x] = y :- b[x] = y"}, `{"v1": "hello", "v2": "goodbye"}`}, - {"no suffix: object incr", []string{"p[x] = y :- q = o, o[x] = y", "q[x] = y :- b[x] = y", "q[x1] = y1 :- b[x1] = y1"}, fmt.Errorf("multiple values for data.q: incremental definitions must produce exactly one value for each key of an object document: check rule definitions: q")}, + {"no suffix: object incr", []string{"p[x] = y :- q = o, o[x] = y", "q[x] = y :- b[x] = y", "q[x1] = y1 :- b[x1] = y1"}, fmt.Errorf("evaluation error (code: 2): multiple values for data.q: rules must produce exactly one value for object document keys: check rule definition(s): q")}, {"no suffix: object incr", []string{"p[x] = y :- q = o, o[x] = y", "q[x] = y :- b[x] = y", `q[x1] = y1 :- d["e"][y1] = x1`}, `{"v1": "hello", "v2": "goodbye", "bar": 0, "baz": 1}`}, {"no suffix: chained", []string{ "p = true :- q = x, x[i] = 4", @@ -548,6 +548,12 @@ func TestTopDownDisjunction(t *testing.T) { {"incr: query object", []string{"p[k] = v :- b[v] = k", "p[k] = v :- a[i] = v, g[k][j] = v"}, `{"b": 2, "c": 4, "hello": "v1", "goodbye": "v2", "a": 1}`}, {"incr: eval set", []string{"p[x] :- q[x]", "q[x] :- a[i] = x", "q[y] :- b[j] = y"}, `[1,2,3,4,"hello","goodbye"]`}, {"incr: eval object", []string{"p[k] = v :- q[k] = v", "q[k] = v :- b[v] = k", "q[k] = v :- a[i] = v, g[k][j] = v"}, `{"b": 2, "c": 4, "hello": "v1", "goodbye": "v2", "a": 1}`}, + {"complete: undefined", []string{"p :- false", "p :- false"}, ""}, + {"complete: error", []string{"p :- true", "p = false :- true"}, fmt.Errorf("evaluation error (code: 2): multiple values for [p]: rules must produce exactly one value for complete documents: check rule definition(s): p")}, + {"complete: valid", []string{"p :- true", "p = true :- true"}, "true"}, + {"complete: valid-2", []string{"p :- true", "p = false :- false"}, "true"}, + {"complete: reference error", []string{"p :- q", "q :- true", "q = false :- true"}, fmt.Errorf("evaluation error (code: 2): multiple values for data.q: rules must produce exactly one value for complete documents: check rule definition(s): q")}, + {"complete: reference valid", []string{"p :- q", "q :- true", "q = true :- true"}, "true"}, } data := loadSmallTestData() @@ -719,7 +725,7 @@ func TestTopDownGlobalVars(t *testing.T) { assertTopDown(t, store, 1, "global vars (missing)", []string{"z", "p"}, `{ req1: {"foo": 4} - }`, unboundGlobalVar(ast.MustParseRef("req2.bar"))) + }`, unboundGlobalVarErr(ast.MustParseRef("req2.bar"))) } func TestExample(t *testing.T) {