From 2f76a155bd1be1bd45dcfe19aebf01986d3a2b0d Mon Sep 17 00:00:00 2001 From: Torin Sandall Date: Tue, 21 Jun 2016 11:07:44 -0700 Subject: [PATCH] Add query support for multiple complete docs Multiple rules may share the same name while defining complete docs as long as the docs generated by the rules at query time do not conflict. A conflict is simply multiple values for the same document. The same applies for object document keys. --- topdown/topdown.go | 74 ++++++++++++++++++++++++++--------------- topdown/topdown_test.go | 12 +++++-- 2 files changed, 57 insertions(+), 29 deletions(-) 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) {