Skip to content

Commit ca96a88

Browse files
authored
chore: query arrow LR implementation (#2817)
1 parent 1898881 commit ca96a88

File tree

2 files changed

+100
-10
lines changed

2 files changed

+100
-10
lines changed

pkg/query/arrow.go

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55

66
"github.com/authzed/spicedb/internal/caveats"
77
core "github.com/authzed/spicedb/pkg/proto/core/v1"
8-
"github.com/authzed/spicedb/pkg/spiceerrors"
98
)
109

1110
// Arrow is an iterator that represents the set of paths that
@@ -180,7 +179,6 @@ func (a *Arrow) IterSubjectsImpl(ctx *Context, resource Object) (PathSeq, error)
180179
Caveat: combinedCaveat,
181180
Expiration: rightPath.Expiration,
182181
Integrity: rightPath.Integrity,
183-
Metadata: make(map[string]any),
184182
}
185183

186184
totalResultPaths++
@@ -197,7 +195,78 @@ func (a *Arrow) IterSubjectsImpl(ctx *Context, resource Object) (PathSeq, error)
197195
}
198196

199197
func (a *Arrow) IterResourcesImpl(ctx *Context, subject ObjectAndRelation) (PathSeq, error) {
200-
return nil, spiceerrors.MustBugf("unimplemented: arrow.go IterResourcesImpl")
198+
// Arrow: resource -> left subjects -> right subjects
199+
// Get resources from right side, then for each, get resources from left side
200+
return func(yield func(Path, error) bool) {
201+
ctx.TraceStep(a, "iterating resources for subject %s:%s", subject.ObjectType, subject.ObjectID)
202+
203+
// Get all resources from the right side
204+
rightSeq, err := ctx.IterResources(a.right, subject)
205+
if err != nil {
206+
yield(Path{}, err)
207+
return
208+
}
209+
210+
rightPathCount := 0
211+
totalResultPaths := 0
212+
for rightPath, err := range rightSeq {
213+
if err != nil {
214+
yield(Path{}, err)
215+
return
216+
}
217+
rightPathCount++
218+
219+
// For each right resource, get resources from left side
220+
// TODO: see if WithEllipses is correct here
221+
rightResourceAsSubject := rightPath.Resource.WithEllipses()
222+
ctx.TraceStep(a, "iterating left side for right resource %s:%s", rightResourceAsSubject.ObjectType, rightResourceAsSubject.ObjectID)
223+
224+
leftSeq, err := ctx.IterResources(a.left, rightResourceAsSubject)
225+
if err != nil {
226+
yield(Path{}, err)
227+
return
228+
}
229+
230+
leftPathCount := 0
231+
for leftPath, err := range leftSeq {
232+
if err != nil {
233+
yield(Path{}, err)
234+
return
235+
}
236+
leftPathCount++
237+
238+
// Combine caveats from both sides (AND logic)
239+
var combinedCaveat *core.CaveatExpression
240+
switch {
241+
case leftPath.Caveat != nil && rightPath.Caveat != nil:
242+
combinedCaveat = caveats.And(leftPath.Caveat, rightPath.Caveat)
243+
case leftPath.Caveat != nil:
244+
combinedCaveat = leftPath.Caveat
245+
case rightPath.Caveat != nil:
246+
combinedCaveat = rightPath.Caveat
247+
}
248+
249+
// Create combined path with resource from right and subject from left
250+
combinedPath := Path{
251+
Resource: leftPath.Resource,
252+
Relation: leftPath.Relation,
253+
Subject: rightPath.Subject,
254+
Caveat: combinedCaveat,
255+
Expiration: rightPath.Expiration,
256+
Integrity: rightPath.Integrity,
257+
}
258+
259+
totalResultPaths++
260+
if !yield(combinedPath, nil) {
261+
return
262+
}
263+
}
264+
265+
ctx.TraceStep(a, "left side returned %d paths for right subject %s:%s", leftPathCount, rightResourceAsSubject.ObjectType, rightResourceAsSubject.ObjectID)
266+
}
267+
268+
ctx.TraceStep(a, "arrow IterSubjects completed: %d right paths, %d total result paths", rightPathCount, totalResultPaths)
269+
}, nil
201270
}
202271

203272
func (a *Arrow) Clone() Iterator {

pkg/query/arrow_test.go

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import (
1111
func TestArrowIterator(t *testing.T) {
1212
t.Parallel()
1313

14-
require := require.New(t)
15-
1614
// Create test iterators using fixed helpers
1715
// Left side: document parent relationships to folders
1816
leftRels := NewFolderHierarchyFixedIterator()
@@ -24,6 +22,7 @@ func TestArrowIterator(t *testing.T) {
2422

2523
t.Run("Check", func(t *testing.T) {
2624
t.Parallel()
25+
require := require.New(t)
2726

2827
// Create context with LocalExecutor
2928
ctx := NewLocalContext(t.Context())
@@ -46,6 +45,7 @@ func TestArrowIterator(t *testing.T) {
4645

4746
t.Run("Check_EmptyResources", func(t *testing.T) {
4847
t.Parallel()
48+
require := require.New(t)
4949

5050
// Create context with LocalExecutor
5151
ctx := NewLocalContext(t.Context())
@@ -60,6 +60,7 @@ func TestArrowIterator(t *testing.T) {
6060

6161
t.Run("Check_NonexistentResource", func(t *testing.T) {
6262
t.Parallel()
63+
require := require.New(t)
6364

6465
// Create context with LocalExecutor
6566
ctx := NewLocalContext(t.Context())
@@ -75,6 +76,7 @@ func TestArrowIterator(t *testing.T) {
7576

7677
t.Run("Check_NoMatchingSubject", func(t *testing.T) {
7778
t.Parallel()
79+
require := require.New(t)
7880

7981
// Create context with LocalExecutor
8082
ctx := NewLocalContext(t.Context())
@@ -90,6 +92,7 @@ func TestArrowIterator(t *testing.T) {
9092

9193
t.Run("IterSubjects", func(t *testing.T) {
9294
t.Parallel()
95+
require := require.New(t)
9396

9497
// Create context with LocalExecutor
9598
ctx := NewLocalContext(t.Context())
@@ -117,15 +120,33 @@ func TestArrowIterator(t *testing.T) {
117120
require.True(foundAlice, "Should find alice as a subject")
118121
})
119122

120-
t.Run("IterResources_Unimplemented", func(t *testing.T) {
123+
t.Run("IterResources", func(t *testing.T) {
121124
t.Parallel()
125+
require := require.New(t)
126+
127+
logger := NewTraceLogger()
122128

123129
// Create context with LocalExecutor
124-
ctx := NewLocalContext(t.Context())
130+
ctx := NewLocalContext(t.Context(), WithTraceLogger(logger))
125131

126-
require.Panics(func() {
127-
_, _ = ctx.IterResources(arrow, NewObject("user", "alice").WithEllipses())
128-
})
132+
// Test arrow IterSubjects: find all resources for a subject through the arrow
133+
// This finds which resources a given subject has access to via the left->right path
134+
pathSeq, err := ctx.IterResources(arrow, NewObject("user", "alice").WithEllipses())
135+
require.NoError(err)
136+
137+
paths, err := CollectAll(pathSeq)
138+
require.NoError(err)
139+
140+
// Expected: spec1 parent is project1, and alice has viewer access to project1
141+
// So spec1 should be returned as a resource
142+
require.NotEmpty(paths, "Should find resources through arrow")
143+
144+
// Verify spec1 is in the results
145+
resourceIds := make([]string, 0, len(paths))
146+
for _, path := range paths {
147+
resourceIds = append(resourceIds, path.Resource.ObjectID)
148+
}
149+
require.Contains(resourceIds, "spec1", "Should find spec1 as a resource")
129150
})
130151
}
131152

0 commit comments

Comments
 (0)