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.
This commit is contained in:
Torin Sandall
2016-06-21 11:07:44 -07:00
parent 386bede297
commit 2f76a155bd
2 changed files with 57 additions and 29 deletions
+48 -26
View File
@@ -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) {
+9 -3
View File
@@ -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) {