Skip to content

Commit a38bbea

Browse files
authored
Merge pull request #2565 from authzed/barakmich/caveat_iterator
feat: Handle caveats and intersection arrows in the query plan
2 parents 041455a + 23a2f27 commit a38bbea

33 files changed

+5948
-203
lines changed

internal/datastore/memdb/readonly.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -493,11 +493,11 @@ func newSubjectSortedIterator(now time.Time, it memdb.ResultIterator, limit *uin
493493
}
494494

495495
if skipCaveats && rt.OptionalCaveat != nil {
496-
return nil, spiceerrors.MustBugf("unexpected caveat in result for relationship: %v", rt)
496+
continue
497497
}
498498

499499
if skipExpiration && rt.OptionalExpiration != nil {
500-
return nil, spiceerrors.MustBugf("unexpected expiration in result for relationship: %v", rt)
500+
continue
501501
}
502502

503503
results = append(results, rt)
@@ -558,13 +558,11 @@ func newMemdbTupleIterator(now time.Time, it memdb.ResultIterator, limit *uint64
558558
}
559559

560560
if skipCaveats && rt.OptionalCaveat != nil {
561-
yield(rt, fmt.Errorf("unexpected caveat in result for relationship: %v", rt))
562-
return
561+
continue
563562
}
564563

565564
if skipExpiration && rt.OptionalExpiration != nil {
566-
yield(rt, fmt.Errorf("unexpected expiration in result for relationship: %v", rt))
567-
return
565+
continue
568566
}
569567

570568
if rt.OptionalExpiration != nil && rt.OptionalExpiration.Before(now) {

internal/services/integrationtesting/query_plan_consistency_test.go

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
package integrationtesting_test
55

66
import (
7+
"fmt"
78
"os"
89
"path/filepath"
10+
"strings"
911
"testing"
1012

1113
"github.com/stretchr/testify/require"
1214

1315
v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
1416

17+
"github.com/authzed/spicedb/internal/caveats"
1518
"github.com/authzed/spicedb/internal/datastore/dsfortesting"
1619
"github.com/authzed/spicedb/internal/datastore/memdb"
1720
"github.com/authzed/spicedb/internal/services/integrationtesting/consistencytestutil"
@@ -50,10 +53,12 @@ type queryPlanConsistencyHandle struct {
5053

5154
func (q *queryPlanConsistencyHandle) buildContext(t *testing.T) *query.Context {
5255
return &query.Context{
53-
Context: t.Context(),
54-
Executor: query.LocalExecutor{},
55-
Datastore: q.ds,
56-
Revision: q.revision,
56+
Context: t.Context(),
57+
Executor: query.LocalExecutor{},
58+
Datastore: q.ds,
59+
Revision: q.revision,
60+
CaveatRunner: caveats.NewCaveatRunner(caveattypes.Default.TypeSet),
61+
TraceLogger: query.NewTraceLogger(), // Enable tracing for debugging
5762
}
5863
}
5964

@@ -115,16 +120,32 @@ func runQueryPlanAssertions(t *testing.T, handle *queryPlanConsistencyHandle) {
115120
it, err := query.BuildIteratorFromSchema(handle.schema, rel.Resource.ObjectType, rel.Resource.Relation)
116121
require.NoError(err)
117122

118-
t.Log(it.Explain())
119-
120123
qctx := handle.buildContext(t)
121124

125+
// Add caveat context from assertion if available
126+
if len(assertion.CaveatContext) > 0 {
127+
qctx.CaveatContext = assertion.CaveatContext
128+
}
129+
122130
seq, err := qctx.Check(it, []query.Object{query.GetObject(rel.Resource)}, rel.Subject)
123131
require.NoError(err)
124132

125133
rels, err := query.CollectAll(seq)
126134
require.NoError(err)
127135

136+
// Print trace if test fails
137+
if qctx.TraceLogger != nil {
138+
defer func() {
139+
if t.Failed() {
140+
t.Logf("Trace for %s:\n%s", entry.name, qctx.TraceLogger.DumpTrace())
141+
// Also print the tree structure for debugging
142+
if it != nil {
143+
t.Logf("Tree structure:\n%s", explainTree(it, 0))
144+
}
145+
}
146+
}()
147+
}
148+
128149
switch entry.expectedPermissionship {
129150
case v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION:
130151
require.Len(rels, 1)
@@ -133,6 +154,9 @@ func runQueryPlanAssertions(t *testing.T, handle *queryPlanConsistencyHandle) {
133154
require.Len(rels, 1)
134155
require.Nil(rels[0].Caveat)
135156
case v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION:
157+
if len(rels) != 0 && qctx.TraceLogger != nil {
158+
t.Logf("Expected 0 relations but got %d. Trace:\n%s", len(rels), qctx.TraceLogger.DumpTrace())
159+
}
136160
require.Len(rels, 0)
137161
}
138162
})
@@ -142,3 +166,21 @@ func runQueryPlanAssertions(t *testing.T, handle *queryPlanConsistencyHandle) {
142166
}
143167
})
144168
}
169+
170+
// explainTree recursively explains the tree structure for debugging
171+
func explainTree(iter query.Iterator, depth int) string {
172+
indent := strings.Repeat(" ", depth)
173+
explain := iter.Explain()
174+
result := fmt.Sprintf("%s%s: %s\n", indent, explain.Name, explain.Info)
175+
176+
for _, subExplain := range explain.SubExplain {
177+
// For SubExplain, we need to create a dummy iterator to get the tree structure
178+
// This is a simplified approach - in practice we'd need access to the actual sub-iterators
179+
subResult := fmt.Sprintf("%s %s: %s\n", indent, subExplain.Name, subExplain.Info)
180+
result += subResult
181+
// Note: We can't recursively call explainTree on SubExplain because it's not an Iterator
182+
// This gives us one level of detail which should be sufficient for debugging
183+
}
184+
185+
return result
186+
}

pkg/query/alias.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func (a *Alias) CheckImpl(ctx *Context, resources []Object, subject ObjectAndRel
3434
}
3535

3636
// Also get relations from sub-iterator
37-
subSeq, err := a.subIt.CheckImpl(ctx, resources, subject)
37+
subSeq, err := ctx.Check(a.subIt, resources, subject)
3838
if err != nil {
3939
return nil, err
4040
}
@@ -62,7 +62,7 @@ func (a *Alias) CheckImpl(ctx *Context, resources []Object, subject ObjectAndRel
6262
}
6363

6464
// No self-edge detected, just rewrite paths from sub-iterator
65-
subSeq, err := a.subIt.CheckImpl(ctx, resources, subject)
65+
subSeq, err := ctx.Check(a.subIt, resources, subject)
6666
if err != nil {
6767
return nil, err
6868
}
@@ -83,7 +83,7 @@ func (a *Alias) CheckImpl(ctx *Context, resources []Object, subject ObjectAndRel
8383
}
8484

8585
func (a *Alias) IterSubjectsImpl(ctx *Context, resource Object) (PathSeq, error) {
86-
subSeq, err := a.subIt.IterSubjectsImpl(ctx, resource)
86+
subSeq, err := ctx.IterSubjects(a.subIt, resource)
8787
if err != nil {
8888
return nil, err
8989
}
@@ -104,7 +104,7 @@ func (a *Alias) IterSubjectsImpl(ctx *Context, resource Object) (PathSeq, error)
104104
}
105105

106106
func (a *Alias) IterResourcesImpl(ctx *Context, subject ObjectAndRelation) (PathSeq, error) {
107-
subSeq, err := a.subIt.IterResourcesImpl(ctx, subject)
107+
subSeq, err := ctx.IterResources(a.subIt, subject)
108108
if err != nil {
109109
return nil, err
110110
}
@@ -133,6 +133,7 @@ func (a *Alias) Clone() Iterator {
133133

134134
func (a *Alias) Explain() Explain {
135135
return Explain{
136+
Name: "Alias",
136137
Info: "Alias(" + a.relation + ")",
137138
SubExplain: []Explain{a.subIt.Explain()},
138139
}

pkg/query/arrow.go

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package query
22

33
import (
4+
"github.com/authzed/spicedb/internal/caveats"
5+
core "github.com/authzed/spicedb/pkg/proto/core/v1"
46
"github.com/authzed/spicedb/pkg/spiceerrors"
57
)
68

@@ -35,45 +37,82 @@ func (a *Arrow) CheckImpl(ctx *Context, resources []Object, subject ObjectAndRel
3537
// don't restructure the tree, but can affect the best way to evaluate the tree, sometimes dynamically.
3638

3739
return func(yield func(Path, error) bool) {
38-
for _, resource := range resources {
39-
subit, err := a.left.IterSubjectsImpl(ctx, resource)
40+
ctx.TraceStep(a, "processing %d resources", len(resources))
41+
42+
totalResultPaths := 0
43+
for resourceIdx, resource := range resources {
44+
ctx.TraceStep(a, "processing resource %d: %s:%s", resourceIdx, resource.ObjectType, resource.ObjectID)
45+
46+
subit, err := ctx.IterSubjects(a.left, resource)
4047
if err != nil {
4148
yield(Path{}, err)
4249
return
4350
}
51+
52+
leftPathCount := 0
4453
for path, err := range subit {
4554
if err != nil {
4655
yield(Path{}, err)
4756
return
4857
}
58+
leftPathCount++
59+
4960
checkResources := []Object{GetObject(path.Subject)}
50-
checkit, err := a.right.CheckImpl(ctx, checkResources, subject)
61+
ctx.TraceStep(a, "checking right side for subject %s:%s", path.Subject.ObjectType, path.Subject.ObjectID)
62+
63+
checkit, err := ctx.Check(a.right, checkResources, subject)
5164
if err != nil {
5265
yield(Path{}, err)
5366
return
5467
}
68+
69+
rightPathCount := 0
5570
for checkPath, err := range checkit {
5671
if err != nil {
5772
yield(Path{}, err)
5873
return
5974
}
75+
rightPathCount++
76+
77+
// Combine caveats from both sides using Path-based approach
78+
// For arrow operations (left->right), both conditions must be satisfied (AND logic)
79+
var combinedCaveat *core.CaveatExpression
80+
if path.Caveat != nil && checkPath.Caveat != nil {
81+
// Both sides have caveats - create combined caveat expression
82+
combinedCaveat = caveats.And(path.Caveat, checkPath.Caveat)
83+
} else if path.Caveat != nil {
84+
// Only left side has caveat
85+
combinedCaveat = path.Caveat
86+
} else if checkPath.Caveat != nil {
87+
// Only right side has caveat
88+
combinedCaveat = checkPath.Caveat
89+
}
90+
// else both are nil, combinedCaveat remains nil
6091

6192
// Create combined path with resource from left and subject from right
6293
combinedPath := Path{
6394
Resource: path.Resource,
6495
Relation: path.Relation,
6596
Subject: checkPath.Subject,
66-
Caveat: checkPath.Caveat,
97+
Caveat: combinedCaveat,
6798
Expiration: checkPath.Expiration,
6899
Integrity: checkPath.Integrity,
69100
Metadata: make(map[string]any),
70101
}
102+
103+
totalResultPaths++
71104
if !yield(combinedPath, nil) {
72105
return
73106
}
74107
}
108+
109+
ctx.TraceStep(a, "right side returned %d paths for subject %s:%s", rightPathCount, path.Subject.ObjectType, path.Subject.ObjectID)
75110
}
111+
112+
ctx.TraceStep(a, "left side returned %d paths for resource %s:%s", leftPathCount, resource.ObjectType, resource.ObjectID)
76113
}
114+
115+
ctx.TraceStep(a, "arrow completed with %d total result paths", totalResultPaths)
77116
}, nil
78117
}
79118

@@ -94,6 +133,7 @@ func (a *Arrow) Clone() Iterator {
94133

95134
func (a *Arrow) Explain() Explain {
96135
return Explain{
136+
Name: "Arrow",
97137
Info: "Arrow",
98138
SubExplain: []Explain{a.left.Explain(), a.right.Explain()},
99139
}

0 commit comments

Comments
 (0)