Skip to content

Commit e49cc12

Browse files
authored
refactor: create new queryopt package to target query plan optimizations (#2970)
1 parent b554261 commit e49cc12

File tree

14 files changed

+590
-639
lines changed

14 files changed

+590
-639
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
55

66
## [Unreleased]
7+
### Added
8+
- New `pkg/query/queryopt` package for building optimizations into the query plan
9+
710
### Changed
811
- Updated CI so that Postgres tests run against v18 which is GA and not against v13 which is EOL (https://github.com/authzed/spicedb/pull/2926)
912
- Added tracing to request validation (https://github.com/authzed/spicedb/pull/2950)

internal/services/integrationtesting/query_plan_consistency_test.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/authzed/spicedb/pkg/datalayer"
2323
"github.com/authzed/spicedb/pkg/datastore"
2424
"github.com/authzed/spicedb/pkg/query"
25+
"github.com/authzed/spicedb/pkg/query/queryopt"
2526
"github.com/authzed/spicedb/pkg/schema/v2"
2627
"github.com/authzed/spicedb/pkg/tuple"
2728
"github.com/authzed/spicedb/pkg/validationfile"
@@ -129,15 +130,18 @@ func runQueryPlanAssertions(t *testing.T, handle *queryPlanConsistencyHandle) {
129130
require := require.New(t)
130131

131132
rel := assertion.Relationship
132-
it, err := query.BuildIteratorFromSchema(handle.schema, rel.Resource.ObjectType, rel.Resource.Relation)
133+
co, err := query.BuildOutlineFromSchema(handle.schema, rel.Resource.ObjectType, rel.Resource.Relation)
133134
require.NoError(err)
134135

135136
// Apply static optimizations if requested
136137
if optimizationMode.optimize {
137-
it, _, err = query.ApplyOptimizations(it, query.StaticOptimizations)
138+
co, err = queryopt.ApplyOptimizations(co, queryopt.StandardOptimzations)
138139
require.NoError(err)
139140
}
140141

142+
it, err := co.Compile()
143+
require.NoError(err)
144+
141145
qctx := handle.buildContext(t)
142146

143147
// Add caveat context from assertion if available
@@ -200,7 +204,9 @@ func runQueryPlanLookupResources(t *testing.T, handle *queryPlanConsistencyHandl
200204
t.Run(tuple.StringONR(subject), func(t *testing.T) {
201205
accessibleResources := accessibilitySet.LookupAccessibleResources(resourceRelation, subject)
202206
queryCtx := handle.buildContext(t)
203-
it, err := query.BuildIteratorFromSchema(handle.schema, resourceRelation.ObjectType, resourceRelation.Relation)
207+
co, err := query.BuildOutlineFromSchema(handle.schema, resourceRelation.ObjectType, resourceRelation.Relation)
208+
require.NoError(t, err)
209+
it, err := co.Compile()
204210
require.NoError(t, err)
205211

206212
// Perform a lookup call and ensure it returns the at least the same set of object IDs.
@@ -254,7 +260,9 @@ func runQueryPlanLookupSubjects(t *testing.T, handle *queryPlanConsistencyHandle
254260
t.Run(tuple.StringONR(resource), func(t *testing.T) {
255261
accessibleSubjects := accessibilitySet.LookupAccessibleSubjects(resource)
256262
queryCtx := handle.buildContext(t)
257-
it, err := query.BuildIteratorFromSchema(handle.schema, resourceRelation.ObjectType, resourceRelation.Relation)
263+
co, err := query.BuildOutlineFromSchema(handle.schema, resourceRelation.ObjectType, resourceRelation.Relation)
264+
require.NoError(t, err)
265+
it, err := co.Compile()
258266
require.NoError(t, err)
259267

260268
// Perform a lookup call and ensure it returns the at least the same set of subject IDs.

internal/services/v1/permissions_queryplan.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/authzed/spicedb/pkg/datastore"
1111
"github.com/authzed/spicedb/pkg/middleware/consistency"
1212
"github.com/authzed/spicedb/pkg/query"
13+
"github.com/authzed/spicedb/pkg/query/queryopt"
1314
"github.com/authzed/spicedb/pkg/schema/v2"
1415
)
1516

@@ -50,15 +51,19 @@ func (ps *permissionServer) checkPermissionWithQueryPlan(ctx context.Context, re
5051
return nil, ps.rewriteError(ctx, err)
5152
}
5253

53-
// Build iterator tree from schema
54-
// TODO: Better iterator caching
55-
it, err := query.BuildIteratorFromSchema(fullSchema, req.Resource.ObjectType, req.Permission)
54+
// Build and optimize the outline, then compile to an iterator tree.
55+
// TODO: Better outline caching
56+
co, err := query.BuildOutlineFromSchema(fullSchema, req.Resource.ObjectType, req.Permission)
5657
if err != nil {
5758
return nil, ps.rewriteError(ctx, err)
5859
}
5960

60-
// Apply basic optimizations to the iterator tree
61-
it, _, err = query.ApplyOptimizations(it, query.StaticOptimizations)
61+
optimized, err := queryopt.ApplyOptimizations(co, queryopt.StandardOptimzations)
62+
if err != nil {
63+
return nil, ps.rewriteError(ctx, err)
64+
}
65+
66+
it, err := optimized.Compile()
6267
if err != nil {
6368
return nil, ps.rewriteError(ctx, err)
6469
}

pkg/query/advisor.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,11 @@ func ApplyAdvisor(co CanonicalOutline, advisor PlanAdvisor) (CanonicalOutline, e
6262
// For each node it calls GetMutations on the advisor and applies the returned
6363
// mutations in sequence. After each mutation it verifies that the resulting
6464
// node's ID matches the original node's ID; a mismatch is a programmer bug.
65+
// After all mutations are applied, any newly synthesized nodes (ID==0) receive
66+
// fresh IDs via FillMissingNodeIDs, and their CanonicalKeys are recorded in
67+
// the provided keys map.
6568
func applyAdvisorMutations(outline Outline, co CanonicalOutline, advisor PlanAdvisor) (Outline, error) {
66-
return WalkOutlineBottomUp(outline, func(node Outline) (Outline, error) {
69+
mutated, err := WalkOutlineBottomUp(outline, func(node Outline) (Outline, error) {
6770
mutations, err := advisor.GetMutations(node, co)
6871
if err != nil {
6972
return Outline{}, err
@@ -81,6 +84,10 @@ func applyAdvisorMutations(outline Outline, co CanonicalOutline, advisor PlanAdv
8184
}
8285
return result, nil
8386
})
87+
if err != nil {
88+
return Outline{}, err
89+
}
90+
return FillMissingNodeIDs(mutated, co.CanonicalKeys), nil
8491
}
8592

8693
// collectAdvisorHints walks the outline tree pre-order via WalkOutlinePreOrder,

pkg/query/build_tree.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,6 @@ type outlineBuilder struct {
2222
recursiveSentinels []*recursiveSentinelInfo // Track recursion points for wrapping in RecursiveIterator
2323
}
2424

25-
// BuildIteratorFromSchema takes a schema and walks the schema tree for a given definition namespace and a relationship or
26-
// permission therein. From this, it generates an iterator tree, rooted on that relationship.
27-
func BuildIteratorFromSchema(fullSchema *schema.Schema, definitionName string, relationName string) (Iterator, error) {
28-
canonical, err := BuildOutlineFromSchema(fullSchema, definitionName, relationName)
29-
if err != nil {
30-
return nil, err
31-
}
32-
return canonical.Compile()
33-
}
34-
3525
// BuildOutlineFromSchema builds a canonical Outline tree from the schema.
3626
func BuildOutlineFromSchema(fullSchema *schema.Schema, definitionName string, relationName string) (CanonicalOutline, error) {
3727
builder := &outlineBuilder{

pkg/query/build_tree_test.go

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@ import (
1414
"github.com/authzed/spicedb/pkg/schema/v2"
1515
)
1616

17+
// buildIterator is a test helper that builds a CanonicalOutline from the schema
18+
// and compiles it into an Iterator. It mirrors the old BuildIteratorFromSchema
19+
// convenience function which has been removed in favour of the explicit
20+
// BuildOutlineFromSchema → Compile() pipeline.
21+
func buildIterator(t *testing.T, fullSchema *schema.Schema, defName, relName string) (Iterator, error) {
22+
t.Helper()
23+
co, err := BuildOutlineFromSchema(fullSchema, defName, relName)
24+
if err != nil {
25+
return nil, err
26+
}
27+
return co.Compile()
28+
}
29+
1730
func TestBuildTree(t *testing.T) {
1831
t.Parallel()
1932

@@ -28,7 +41,7 @@ func TestBuildTree(t *testing.T) {
2841
dsSchema, err := schema.BuildSchemaFromDefinitions(objectDefs, nil)
2942
require.NoError(err)
3043

31-
it, err := BuildIteratorFromSchema(dsSchema, "document", "edit")
44+
it, err := buildIterator(t, dsSchema, "document", "edit")
3245
require.NoError(err)
3346

3447
ctx := NewLocalContext(t.Context(),
@@ -55,7 +68,7 @@ func TestBuildTreeMultipleRelations(t *testing.T) {
5568
require.NoError(err)
5669

5770
// Test building iterator for edit permission which creates a union
58-
it, err := BuildIteratorFromSchema(dsSchema, "document", "edit")
71+
it, err := buildIterator(t, dsSchema, "document", "edit")
5972
require.NoError(err)
6073

6174
explain := it.Explain()
@@ -81,12 +94,12 @@ func TestBuildTreeInvalidDefinition(t *testing.T) {
8194
require.NoError(err)
8295

8396
// Test with invalid definition name
84-
_, err = BuildIteratorFromSchema(dsSchema, "nonexistent", "edit")
97+
_, err = buildIterator(t, dsSchema, "nonexistent", "edit")
8598
require.Error(err)
8699
require.Contains(err.Error(), "couldn't find a schema definition named `nonexistent`")
87100

88101
// Test with invalid relation/permission name
89-
_, err = BuildIteratorFromSchema(dsSchema, "document", "nonexistent")
102+
_, err = buildIterator(t, dsSchema, "document", "nonexistent")
90103
require.Error(err)
91104
require.Contains(err.Error(), "couldn't find a relation or permission named `nonexistent`")
92105
}
@@ -105,7 +118,7 @@ func TestBuildTreeSubRelations(t *testing.T) {
105118
require.NoError(err)
106119

107120
// Test building iterator for a relation with subrelations
108-
it, err := BuildIteratorFromSchema(dsSchema, "document", "parent")
121+
it, err := buildIterator(t, dsSchema, "document", "parent")
109122
require.NoError(err)
110123

111124
// Should have created a relation iterator
@@ -152,7 +165,7 @@ func TestBuildTreeRecursion(t *testing.T) {
152165

153166
// This should detect recursion and create a RecursiveIterator
154167
// The arrow operation parent->member creates recursion: group->parent->member->parent->member...
155-
it, err := BuildIteratorFromSchema(dsSchema, "group", "member")
168+
it, err := buildIterator(t, dsSchema, "group", "member")
156169
require.NoError(err)
157170
require.NotNil(it)
158171

@@ -203,7 +216,7 @@ func TestBuildTreeIntersectionOperation(t *testing.T) {
203216
require.NoError(err)
204217

205218
// Test building iterator for view_and_edit permission which uses intersection operations
206-
it, err := BuildIteratorFromSchema(dsSchema, "document", "view_and_edit")
219+
it, err := buildIterator(t, dsSchema, "document", "view_and_edit")
207220
require.NoError(err)
208221

209222
// Should create an intersection iterator
@@ -245,7 +258,7 @@ func TestBuildTreeExclusionOperation(t *testing.T) {
245258
require.NoError(err)
246259

247260
// Test building iterator for exclusion permission - should succeed
248-
it, err := BuildIteratorFromSchema(dsSchema, "document", "excluded_perm")
261+
it, err := buildIterator(t, dsSchema, "document", "excluded_perm")
249262
require.NoError(err)
250263
require.NotNil(it)
251264
// Should be wrapped in an Alias
@@ -297,7 +310,7 @@ func TestBuildTreeExclusionEdgeCases(t *testing.T) {
297310
dsSchema, err := schema.BuildSchemaFromDefinitions(objectDefs, nil)
298311
require.NoError(err)
299312

300-
it, err := BuildIteratorFromSchema(dsSchema, "document", "can_view")
313+
it, err := buildIterator(t, dsSchema, "document", "can_view")
301314
require.NoError(err)
302315
require.NotNil(it)
303316
// Should be wrapped in an Alias
@@ -336,7 +349,7 @@ func TestBuildTreeExclusionEdgeCases(t *testing.T) {
336349
dsSchema, err := schema.BuildSchemaFromDefinitions(objectDefs, nil)
337350
require.NoError(err)
338351

339-
it, err := BuildIteratorFromSchema(dsSchema, "document", "restricted_viewers")
352+
it, err := buildIterator(t, dsSchema, "document", "restricted_viewers")
340353
require.NoError(err)
341354
require.NotNil(it)
342355
// Should be wrapped in an Alias
@@ -377,7 +390,7 @@ func TestBuildTreeExclusionEdgeCases(t *testing.T) {
377390
dsSchema, err := schema.BuildSchemaFromDefinitions(objectDefs, nil)
378391
require.NoError(err)
379392

380-
it, err := BuildIteratorFromSchema(dsSchema, "document", "restricted_view")
393+
it, err := buildIterator(t, dsSchema, "document", "restricted_view")
381394
require.NoError(err)
382395
require.NotNil(it)
383396
// Should be wrapped in an Alias
@@ -419,7 +432,7 @@ func TestBuildTreeExclusionEdgeCases(t *testing.T) {
419432
dsSchema, err := schema.BuildSchemaFromDefinitions(objectDefs, nil)
420433
require.NoError(err)
421434

422-
it, err := BuildIteratorFromSchema(dsSchema, "document", "allowed_users")
435+
it, err := buildIterator(t, dsSchema, "document", "allowed_users")
423436
require.NoError(err)
424437
require.NotNil(it)
425438
// Should be wrapped in an Alias
@@ -457,7 +470,7 @@ func TestBuildTreeExclusionEdgeCases(t *testing.T) {
457470
require.NoError(err)
458471

459472
// Building iterator should fail due to missing relation
460-
_, err = BuildIteratorFromSchema(dsSchema, "document", "bad_exclusion")
473+
_, err = buildIterator(t, dsSchema, "document", "bad_exclusion")
461474
require.Error(err)
462475
require.Contains(err.Error(), "couldn't find a relation or permission named `nonexistent_relation`")
463476
})
@@ -480,7 +493,7 @@ func TestBuildTreeExclusionEdgeCases(t *testing.T) {
480493
require.NoError(err)
481494

482495
// Building iterator should fail due to missing relation
483-
_, err = BuildIteratorFromSchema(dsSchema, "document", "bad_exclusion")
496+
_, err = buildIterator(t, dsSchema, "document", "bad_exclusion")
484497
require.Error(err)
485498
require.Contains(err.Error(), "couldn't find a relation or permission named `nonexistent_relation`")
486499
})
@@ -507,7 +520,7 @@ func TestBuildTreeArrowMissingLeftRelation(t *testing.T) {
507520
require.NoError(err)
508521

509522
// Test building iterator for arrow with missing left relation
510-
_, err = BuildIteratorFromSchema(dsSchema, "document", "bad_arrow")
523+
_, err = buildIterator(t, dsSchema, "document", "bad_arrow")
511524
require.Error(err)
512525
require.Contains(err.Error(), "couldn't find left-hand relation for arrow")
513526
}
@@ -526,7 +539,7 @@ func TestBuildTreeSingleRelationOptimization(t *testing.T) {
526539
require.NoError(err)
527540

528541
// Test building iterator for a simple relation - should not create unnecessary unions
529-
it, err := BuildIteratorFromSchema(dsSchema, "document", "owner")
542+
it, err := buildIterator(t, dsSchema, "document", "owner")
530543
require.NoError(err)
531544

532545
// Should create a simple relation iterator without extra union wrappers
@@ -580,7 +593,7 @@ func TestBuildTreeSubrelationHandling(t *testing.T) {
580593
require.NoError(err)
581594

582595
// Should create an alias wrapping union with arrow
583-
it, err := BuildIteratorFromSchema(dsSchema, "document", "viewer")
596+
it, err := buildIterator(t, dsSchema, "document", "viewer")
584597
require.NoError(err)
585598
require.NotNil(it)
586599

@@ -617,7 +630,7 @@ func TestBuildTreeSubrelationHandling(t *testing.T) {
617630
dsSchema, err := schema.BuildSchemaFromDefinitions(objectDefs, nil)
618631
require.NoError(err)
619632

620-
it, err := BuildIteratorFromSchema(dsSchema, "document", "viewer")
633+
it, err := buildIterator(t, dsSchema, "document", "viewer")
621634
require.NoError(err)
622635
require.NotNil(it)
623636

@@ -649,7 +662,7 @@ func TestBuildTreeSubrelationHandling(t *testing.T) {
649662
require.NoError(err)
650663

651664
// Should create RecursiveIterator for arrow recursion
652-
it, err := BuildIteratorFromSchema(dsSchema, "document", "viewer")
665+
it, err := buildIterator(t, dsSchema, "document", "viewer")
653666
require.NoError(err)
654667
require.NotNil(it)
655668

@@ -680,7 +693,7 @@ func TestBuildTreeSubrelationHandling(t *testing.T) {
680693
require.NoError(err)
681694

682695
// Should fail when trying to build iterator due to missing subrelation
683-
_, err = BuildIteratorFromSchema(dsSchema, "document", "viewer")
696+
_, err = buildIterator(t, dsSchema, "document", "viewer")
684697
require.Error(err)
685698
require.Contains(err.Error(), "couldn't find a relation or permission named `nonexistent`")
686699
})
@@ -708,7 +721,7 @@ func TestBuildTreeSubrelationHandling(t *testing.T) {
708721
dsSchema, err := schema.BuildSchemaFromDefinitions(objectDefs, nil)
709722
require.NoError(err)
710723

711-
it, err := BuildIteratorFromSchema(dsSchema, "document", "viewer")
724+
it, err := buildIterator(t, dsSchema, "document", "viewer")
712725
require.NoError(err)
713726
require.NotNil(it)
714727

@@ -770,7 +783,7 @@ func TestBuildTreeWildcardIterator(t *testing.T) {
770783

771784
t.Run("Schema with wildcard creates WildcardIterator", func(t *testing.T) {
772785
t.Parallel()
773-
it, err := BuildIteratorFromSchema(dsSchema, "document", "viewer")
786+
it, err := buildIterator(t, dsSchema, "document", "viewer")
774787
require.NoError(err)
775788
require.NotNil(it)
776789

@@ -801,7 +814,7 @@ func TestBuildTreeWildcardIterator(t *testing.T) {
801814
mixedSchema, err := schema.BuildSchemaFromDefinitions(mixedObjectDefs, nil)
802815
require.NoError(err)
803816

804-
it, err := BuildIteratorFromSchema(mixedSchema, "document", "viewer")
817+
it, err := buildIterator(t, mixedSchema, "document", "viewer")
805818
require.NoError(err)
806819
require.NotNil(it)
807820

@@ -859,7 +872,7 @@ func TestBuildTreeMutualRecursionSentinelFiltering(t *testing.T) {
859872
t.Run("document viewer builds successfully with mutual recursion", func(t *testing.T) {
860873
t.Parallel()
861874
// Build iterator for document#viewer - should detect recursion and wrap properly
862-
it, err := BuildIteratorFromSchema(dsSchema, "document", "viewer")
875+
it, err := buildIterator(t, dsSchema, "document", "viewer")
863876
require.NoError(err)
864877
require.NotNil(it)
865878

@@ -872,7 +885,7 @@ func TestBuildTreeMutualRecursionSentinelFiltering(t *testing.T) {
872885
t.Run("otherdocument viewer builds successfully with mutual recursion", func(t *testing.T) {
873886
t.Parallel()
874887
// Build iterator for otherdocument#viewer - should also handle mutual recursion
875-
it, err := BuildIteratorFromSchema(dsSchema, "otherdocument", "viewer")
888+
it, err := buildIterator(t, dsSchema, "otherdocument", "viewer")
876889
require.NoError(err)
877890
require.NotNil(it)
878891

@@ -890,7 +903,7 @@ func TestBuildTreeMutualRecursionSentinelFiltering(t *testing.T) {
890903
// its own sentinels.
891904

892905
// Build the tree
893-
it, err := BuildIteratorFromSchema(dsSchema, "document", "viewer")
906+
it, err := buildIterator(t, dsSchema, "document", "viewer")
894907
require.NoError(err)
895908
require.NotNil(it)
896909

0 commit comments

Comments
 (0)