11package query
22
33import (
4- "errors"
54 "fmt"
65
76 core "github.com/authzed/spicedb/pkg/proto/core/v1"
87 "github.com/authzed/spicedb/pkg/schema/v2"
8+ "github.com/authzed/spicedb/pkg/spiceerrors"
99 "github.com/authzed/spicedb/pkg/tuple"
1010)
1111
12+ type recursiveSentinelInfo struct {
13+ sentinel * RecursiveSentinel
14+ definitionName string
15+ relationName string
16+ }
17+
1218type iteratorBuilder struct {
13- schema * schema.Schema
14- seen map [string ]bool
15- collectedCaveats []* core.ContextualizedCaveat // Collect caveats to combine with AND logic
19+ schema * schema.Schema
20+ building map [string ]bool // Track what's currently being built (call stack)
21+ collectedCaveats []* core.ContextualizedCaveat // Collect caveats to combine with AND logic
22+ recursiveSentinels []* recursiveSentinelInfo // Track recursion points for wrapping in RecursiveIterator
1623}
1724
1825// BuildIteratorFromSchema takes a schema and walks the schema tree for a given definition namespace and a relationship or
1926// permission therein. From this, it generates an iterator tree, rooted on that relationship.
2027func BuildIteratorFromSchema (fullSchema * schema.Schema , definitionName string , relationName string ) (Iterator , error ) {
2128 builder := & iteratorBuilder {
22- schema : fullSchema ,
23- seen : make (map [string ]bool ),
24- collectedCaveats : make ([]* core.ContextualizedCaveat , 0 ),
29+ schema : fullSchema ,
30+ building : make (map [string ]bool ),
31+ collectedCaveats : make ([]* core.ContextualizedCaveat , 0 ),
32+ recursiveSentinels : make ([]* recursiveSentinelInfo , 0 ),
2533 }
2634 iterator , err := builder .buildIteratorFromSchemaInternal (definitionName , relationName , true )
2735 if err != nil {
@@ -33,27 +41,80 @@ func BuildIteratorFromSchema(fullSchema *schema.Schema, definitionName string, r
3341 for _ , caveat := range builder .collectedCaveats {
3442 result = NewCaveatIterator (result , caveat )
3543 }
44+
45+ // Note: RecursiveIterator wrapping happens at the recursion point,
46+ // not at the top level. So we shouldn't have any sentinels left here.
47+ if len (builder .recursiveSentinels ) > 0 {
48+ // This would be an error - sentinels should have been wrapped already
49+ return nil , spiceerrors .MustBugf ("unwrapped sentinels remaining: %d" , len (builder .recursiveSentinels ))
50+ }
51+
3652 return result , nil
3753}
3854
3955func (b * iteratorBuilder ) buildIteratorFromSchemaInternal (definitionName string , relationName string , withSubRelations bool ) (Iterator , error ) {
40- id := fmt .Sprintf ("%s#%s:%v" , definitionName , relationName , withSubRelations )
41- if b .seen [id ] {
42- return nil , errors .New ("recursive schema iterators are as yet unsupported" )
56+ id := fmt .Sprintf ("%s#%s" , definitionName , relationName )
57+
58+ // Check if we're currently building this (true recursion)
59+ // Check both with the same flag and opposite flag, since recursion can cross the boundary
60+ if b .building [id ] {
61+ // Recursion detected - create sentinel and remember where
62+ sentinel := NewRecursiveSentinel (definitionName , relationName , withSubRelations )
63+ // Track this sentinel with its location info
64+ sentinelInfo := & recursiveSentinelInfo {
65+ sentinel : sentinel ,
66+ definitionName : definitionName ,
67+ relationName : relationName ,
68+ }
69+ b .recursiveSentinels = append (b .recursiveSentinels , sentinelInfo )
70+ return sentinel , nil
4371 }
44- b .seen [id ] = true
72+
73+ // Mark as currently building
74+ b .building [id ] = true
75+ // Track the position in the sentinels list before building
76+ sentinelsLenBefore := len (b .recursiveSentinels )
4577
4678 def , ok := b .schema .Definitions ()[definitionName ]
4779 if ! ok {
80+ // Remove before returning error
81+ delete (b .building , id )
4882 return nil , fmt .Errorf ("BuildIteratorFromSchema: couldn't find a schema definition named `%s`" , definitionName )
4983 }
84+
85+ var result Iterator
86+ var err error
5087 if p , ok := def .Permissions ()[relationName ]; ok {
51- return b .buildIteratorFromPermission (p )
88+ result , err = b .buildIteratorFromPermission (p )
89+ } else if r , ok := def .Relations ()[relationName ]; ok {
90+ result , err = b .buildIteratorFromRelation (r , withSubRelations )
91+ } else {
92+ err = fmt .Errorf ("BuildIteratorFromSchema: couldn't find a relation or permission named `%s` in definition `%s`" , relationName , definitionName )
93+ }
94+
95+ // Remove from building after we're done (allows reuse in other branches)
96+ delete (b .building , id )
97+
98+ if err != nil {
99+ return nil , err
52100 }
53- if r , ok := def .Relations ()[relationName ]; ok {
54- return b .buildIteratorFromRelation (r , withSubRelations )
101+
102+ // Check if any NEW sentinels were added while building this
103+ // If so, this subtree contains recursion and should be wrapped
104+ sentinelsAdded := b .recursiveSentinels [sentinelsLenBefore :]
105+ if len (sentinelsAdded ) > 0 {
106+ // Extract just the sentinel objects
107+ sentinels := make ([]* RecursiveSentinel , len (sentinelsAdded ))
108+ for i , info := range sentinelsAdded {
109+ sentinels [i ] = info .sentinel
110+ }
111+ // Wrap this subtree in RecursiveIterator
112+ result = NewRecursiveIterator (result )
113+ // Remove these sentinels from the list since we've wrapped them
114+ b .recursiveSentinels = b .recursiveSentinels [:sentinelsLenBefore ]
55115 }
56- return nil , fmt .Errorf ("BuildIteratorFromSchema: couldn't find a relation or permission named `%s` in definition `%s`" , relationName , definitionName )
116+
117+ return result , nil
57118}
58119
59120func (b * iteratorBuilder ) buildIteratorFromRelation (r * schema.Relation , withSubRelations bool ) (Iterator , error ) {
@@ -177,12 +238,22 @@ func (b *iteratorBuilder) buildBaseRelationIterator(br *schema.BaseRelation, wit
177238 return base , nil
178239 }
179240
180- // For relation references in schema definitions (like group#member in "relation member: user | group#member"),
181- // we always need to resolve what the referenced relation means, even if withSubRelations=false.
182- // The withSubRelations flag controls whether we build arrows for nested traversal, but relation
183- // references in the schema definition itself must always be resolved.
184- // However, we still need to prevent infinite recursion.
185- if ! withSubRelations {
241+ // Check if we need to expand subrelations
242+ // We always need to expand if withSubRelations=true (normal case)
243+ // OR if the subrelation might be recursive (same type as something we're building)
244+ needsExpansion := withSubRelations
245+
246+ if ! needsExpansion {
247+ // Check if this might be a recursive subrelation
248+ // by seeing if the subrelation type matches any definition we're currently building
249+ subrelID := fmt .Sprintf ("%s#%s" , br .Type (), br .Subrelation ())
250+ if b .building [subrelID ] {
251+ // This is recursive! We need to expand to detect it
252+ needsExpansion = true
253+ }
254+ }
255+
256+ if ! needsExpansion {
186257 return base , nil
187258 }
188259
@@ -191,8 +262,7 @@ func (b *iteratorBuilder) buildBaseRelationIterator(br *schema.BaseRelation, wit
191262 return nil , err
192263 }
193264
194- // We must check the effective arrow of a subrelation if we have one and subrelations are enabled
195- // (subrelations are disabled in cases of actual arrows)
265+ // We must check the effective arrow of a subrelation if we have one
196266 union := NewUnion ()
197267 union .addSubIterator (base )
198268
0 commit comments