Skip to content

Commit b1837a8

Browse files
authored
chore: intersection lr implementation (#2816)
1 parent 14ae216 commit b1837a8

File tree

2 files changed

+103
-7
lines changed

2 files changed

+103
-7
lines changed

pkg/query/intersection.go

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package query
22

33
import (
44
"github.com/google/uuid"
5-
6-
"github.com/authzed/spicedb/pkg/spiceerrors"
75
)
86

97
// Intersection the set of paths that are in all of underlying subiterators.
@@ -221,7 +219,92 @@ func (i *Intersection) IterSubjectsImpl(ctx *Context, resource Object) (PathSeq,
221219
}
222220

223221
func (i *Intersection) IterResourcesImpl(ctx *Context, subject ObjectAndRelation) (PathSeq, error) {
224-
return nil, spiceerrors.MustBugf("unimplemented: intersection.go IterResourcesImpl")
222+
ctx.TraceStep(i, "iterating resources for subject %s:%s from %d sub-iterators", subject.ObjectType, subject.ObjectID, len(i.subIts))
223+
224+
// Track paths by resource key for combining with AND logic
225+
pathsByKey := make(map[string]Path)
226+
227+
for iterIdx, it := range i.subIts {
228+
ctx.TraceStep(i, "processing sub-iterator %d", iterIdx)
229+
230+
pathSeq, err := ctx.IterResources(it, subject)
231+
if err != nil {
232+
return nil, err
233+
}
234+
paths, err := CollectAll(pathSeq)
235+
if err != nil {
236+
return nil, err
237+
}
238+
239+
ctx.TraceStep(i, "sub-iterator %d returned %d paths", iterIdx, len(paths))
240+
241+
if len(paths) == 0 {
242+
ctx.TraceStep(i, "sub-iterator %d returned empty, short-circuiting", iterIdx)
243+
return EmptyPathSeq(), nil
244+
}
245+
246+
if iterIdx == 0 {
247+
// First iterator - initialize pathsByKey using resource-based keys
248+
for _, path := range paths {
249+
key := path.Resource.Key()
250+
if existing, exists := pathsByKey[key]; !exists {
251+
pathsByKey[key] = path
252+
} else {
253+
// If multiple paths for same resource in first iterator, merge with OR
254+
merged, err := existing.MergeOr(path)
255+
if err != nil {
256+
return nil, err
257+
}
258+
pathsByKey[key] = merged
259+
}
260+
}
261+
} else {
262+
// Subsequent iterators - intersect based on resources and combine caveats
263+
newPathsByKey := make(map[string]Path)
264+
265+
// First collect all paths from this iterator by resource
266+
currentIterPaths := make(map[string]Path)
267+
for _, path := range paths {
268+
key := path.Resource.Key()
269+
if existing, exists := currentIterPaths[key]; !exists {
270+
currentIterPaths[key] = path
271+
} else {
272+
// Multiple paths for same resource in current iterator, merge with OR
273+
merged, err := existing.MergeOr(path)
274+
if err != nil {
275+
return nil, err
276+
}
277+
currentIterPaths[key] = merged
278+
}
279+
}
280+
281+
// Now intersect: only keep subjects that exist in both previous and current
282+
for key, currentPath := range currentIterPaths {
283+
if existing, exists := pathsByKey[key]; exists {
284+
// Combine using intersection logic (AND)
285+
combined, err := existing.MergeAnd(currentPath)
286+
if err != nil {
287+
return nil, err
288+
}
289+
newPathsByKey[key] = combined
290+
}
291+
// If resource not in previous results, it's filtered out (intersection)
292+
}
293+
pathsByKey = newPathsByKey
294+
295+
if len(pathsByKey) == 0 {
296+
return EmptyPathSeq(), nil
297+
}
298+
}
299+
}
300+
301+
return func(yield func(Path, error) bool) {
302+
for _, path := range pathsByKey {
303+
if !yield(path, nil) {
304+
return
305+
}
306+
}
307+
}, nil
225308
}
226309

227310
func (i *Intersection) Clone() Iterator {

pkg/query/intersection_test.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,26 @@ func TestIntersectionIterator(t *testing.T) {
167167
require.NotEmpty(paths, "Intersection should find common subjects")
168168
})
169169

170-
t.Run("IterResources_Unimplemented", func(t *testing.T) {
170+
t.Run("IterResources", func(t *testing.T) {
171171
t.Parallel()
172172

173173
intersect := NewIntersection()
174-
require.Panics(func() {
175-
_, _ = ctx.IterResources(intersect, NewObject("user", "alice").WithEllipses())
176-
})
174+
175+
// Add test iterators
176+
documentAccess := NewDocumentAccessFixedIterator()
177+
multiRole := NewMultiRoleFixedIterator()
178+
179+
intersect.addSubIterator(documentAccess)
180+
intersect.addSubIterator(multiRole)
181+
182+
pathSeq, err := ctx.IterResources(intersect, NewObject("user", "alice").WithEllipses())
183+
require.NoError(err)
184+
185+
paths, err := CollectAll(pathSeq)
186+
require.NoError(err)
187+
188+
// Should return resources that are in BOTH iterators
189+
require.NotEmpty(paths, "Intersection should find common resources")
177190
})
178191
}
179192

0 commit comments

Comments
 (0)