Skip to content

Commit b15effc

Browse files
authored
feat(queryopt): set simplification optimizer (#3051)
1 parent ee7c9a7 commit b15effc

8 files changed

Lines changed: 1238 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
77
### Added
88
- Add DispatchExecutor, a query plan executor that is Dispatch-aware and sends subproblems on Alias boundaries (https://github.com/authzed/spicedb/pull/3074)
99
- Implement Dispatch caching for query plan execution (https://github.com/authzed/spicedb/pull/3079)
10+
- Add new optimizer to query planner based on set theory laws for simplifications (https://github.com/authzed/spicedb/pull/3051)
1011

1112
### Changed
1213
- Build: strip quarantine attribute for MacOS (https://github.com/authzed/spicedb/pull/3082)

pkg/query/mutations.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func MutateOutline(outline Outline, fns []OutlineMutation) Outline {
2222
// upward through the tree according to each node type's semantics:
2323
// - Union: null if ALL children are null
2424
// - Intersection: null if ANY child is null
25-
// - Arrow/IntersectionArrow: null if the right child is null
25+
// - Arrow/IntersectionArrow: null if either child is null
2626
// - Exclusion: null if the left child is null
2727
// - Caveat/Alias/Recursive: null if the only child is null
2828
//
@@ -47,7 +47,7 @@ func NullPropagation(outline Outline) Outline {
4747
}
4848

4949
case ArrowIteratorType, IntersectionArrowIteratorType:
50-
if len(outline.SubOutlines) == 2 && outline.SubOutlines[1].Type == NullIteratorType {
50+
if len(outline.SubOutlines) == 2 && (outline.SubOutlines[0].Type == NullIteratorType || outline.SubOutlines[1].Type == NullIteratorType) {
5151
return Outline{Type: NullIteratorType, ID: outline.ID}
5252
}
5353

pkg/query/mutations_test.go

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,268 @@ import (
66
"github.com/stretchr/testify/require"
77
)
88

9+
func TestNullPropagation(t *testing.T) {
10+
live := Outline{Type: FixedIteratorType}
11+
null := Outline{Type: NullIteratorType}
12+
13+
// --- UnionIteratorType ---
14+
15+
t.Run("union: all null → null", func(t *testing.T) {
16+
result := NullPropagation(Outline{Type: UnionIteratorType, SubOutlines: []Outline{null, null}})
17+
require.Equal(t, NullIteratorType, result.Type)
18+
})
19+
20+
t.Run("union: all null (3 children) → null", func(t *testing.T) {
21+
result := NullPropagation(Outline{Type: UnionIteratorType, SubOutlines: []Outline{null, null, null}})
22+
require.Equal(t, NullIteratorType, result.Type)
23+
})
24+
25+
t.Run("union: one live child → unchanged", func(t *testing.T) {
26+
result := NullPropagation(Outline{Type: UnionIteratorType, SubOutlines: []Outline{live, null}})
27+
require.Equal(t, UnionIteratorType, result.Type)
28+
})
29+
30+
t.Run("union: mixed (3 children, 1 live) → unchanged", func(t *testing.T) {
31+
result := NullPropagation(Outline{Type: UnionIteratorType, SubOutlines: []Outline{null, live, null}})
32+
require.Equal(t, UnionIteratorType, result.Type)
33+
})
34+
35+
t.Run("union: all live → unchanged", func(t *testing.T) {
36+
result := NullPropagation(Outline{Type: UnionIteratorType, SubOutlines: []Outline{live, live}})
37+
require.Equal(t, UnionIteratorType, result.Type)
38+
})
39+
40+
t.Run("union: preserves ID when null", func(t *testing.T) {
41+
result := NullPropagation(Outline{Type: UnionIteratorType, ID: 7, SubOutlines: []Outline{null, null}})
42+
require.Equal(t, NullIteratorType, result.Type)
43+
require.Equal(t, OutlineNodeID(7), result.ID)
44+
})
45+
46+
// --- IntersectionIteratorType ---
47+
48+
t.Run("intersection: any null child → null", func(t *testing.T) {
49+
result := NullPropagation(Outline{Type: IntersectionIteratorType, SubOutlines: []Outline{live, null}})
50+
require.Equal(t, NullIteratorType, result.Type)
51+
})
52+
53+
t.Run("intersection: null first child → null", func(t *testing.T) {
54+
result := NullPropagation(Outline{Type: IntersectionIteratorType, SubOutlines: []Outline{null, live}})
55+
require.Equal(t, NullIteratorType, result.Type)
56+
})
57+
58+
t.Run("intersection: all null → null", func(t *testing.T) {
59+
result := NullPropagation(Outline{Type: IntersectionIteratorType, SubOutlines: []Outline{null, null}})
60+
require.Equal(t, NullIteratorType, result.Type)
61+
})
62+
63+
t.Run("intersection: one null in 3 children → null", func(t *testing.T) {
64+
result := NullPropagation(Outline{Type: IntersectionIteratorType, SubOutlines: []Outline{live, null, live}})
65+
require.Equal(t, NullIteratorType, result.Type)
66+
})
67+
68+
t.Run("intersection: all live → unchanged", func(t *testing.T) {
69+
result := NullPropagation(Outline{Type: IntersectionIteratorType, SubOutlines: []Outline{live, live}})
70+
require.Equal(t, IntersectionIteratorType, result.Type)
71+
})
72+
73+
t.Run("intersection: preserves ID when null", func(t *testing.T) {
74+
result := NullPropagation(Outline{Type: IntersectionIteratorType, ID: 8, SubOutlines: []Outline{live, null}})
75+
require.Equal(t, NullIteratorType, result.Type)
76+
require.Equal(t, OutlineNodeID(8), result.ID)
77+
})
78+
79+
// --- ArrowIteratorType ---
80+
81+
t.Run("arrow: null left child → null", func(t *testing.T) {
82+
// Null → B has no sources to traverse, so result is empty.
83+
result := NullPropagation(Outline{Type: ArrowIteratorType, SubOutlines: []Outline{null, live}})
84+
require.Equal(t, NullIteratorType, result.Type)
85+
})
86+
87+
t.Run("arrow: null right child → null", func(t *testing.T) {
88+
// A → Null has no target to reach.
89+
result := NullPropagation(Outline{Type: ArrowIteratorType, SubOutlines: []Outline{live, null}})
90+
require.Equal(t, NullIteratorType, result.Type)
91+
})
92+
93+
t.Run("arrow: both children null → null", func(t *testing.T) {
94+
result := NullPropagation(Outline{Type: ArrowIteratorType, SubOutlines: []Outline{null, null}})
95+
require.Equal(t, NullIteratorType, result.Type)
96+
})
97+
98+
t.Run("arrow: neither child null → unchanged", func(t *testing.T) {
99+
result := NullPropagation(Outline{Type: ArrowIteratorType, SubOutlines: []Outline{live, live}})
100+
require.Equal(t, ArrowIteratorType, result.Type)
101+
})
102+
103+
t.Run("arrow: wrong child count (1) → unchanged (defensive guard)", func(t *testing.T) {
104+
result := NullPropagation(Outline{Type: ArrowIteratorType, SubOutlines: []Outline{null}})
105+
require.Equal(t, ArrowIteratorType, result.Type)
106+
})
107+
108+
t.Run("arrow: preserves ID when null", func(t *testing.T) {
109+
result := NullPropagation(Outline{Type: ArrowIteratorType, ID: 42, SubOutlines: []Outline{null, live}})
110+
require.Equal(t, NullIteratorType, result.Type)
111+
require.Equal(t, OutlineNodeID(42), result.ID)
112+
})
113+
114+
// --- IntersectionArrowIteratorType ---
115+
116+
t.Run("intersection arrow: null left child → null", func(t *testing.T) {
117+
result := NullPropagation(Outline{Type: IntersectionArrowIteratorType, SubOutlines: []Outline{null, live}})
118+
require.Equal(t, NullIteratorType, result.Type)
119+
})
120+
121+
t.Run("intersection arrow: null right child → null", func(t *testing.T) {
122+
result := NullPropagation(Outline{Type: IntersectionArrowIteratorType, SubOutlines: []Outline{live, null}})
123+
require.Equal(t, NullIteratorType, result.Type)
124+
})
125+
126+
t.Run("intersection arrow: both children null → null", func(t *testing.T) {
127+
result := NullPropagation(Outline{Type: IntersectionArrowIteratorType, SubOutlines: []Outline{null, null}})
128+
require.Equal(t, NullIteratorType, result.Type)
129+
})
130+
131+
t.Run("intersection arrow: neither child null → unchanged", func(t *testing.T) {
132+
result := NullPropagation(Outline{Type: IntersectionArrowIteratorType, SubOutlines: []Outline{live, live}})
133+
require.Equal(t, IntersectionArrowIteratorType, result.Type)
134+
})
135+
136+
t.Run("intersection arrow: wrong child count (1) → unchanged (defensive guard)", func(t *testing.T) {
137+
result := NullPropagation(Outline{Type: IntersectionArrowIteratorType, SubOutlines: []Outline{null}})
138+
require.Equal(t, IntersectionArrowIteratorType, result.Type)
139+
})
140+
141+
t.Run("intersection arrow: wrong child count (3) with null child → unchanged (defensive guard)", func(t *testing.T) {
142+
result := NullPropagation(Outline{Type: IntersectionArrowIteratorType, SubOutlines: []Outline{null, live, live}})
143+
require.Equal(t, IntersectionArrowIteratorType, result.Type)
144+
})
145+
146+
t.Run("intersection arrow: preserves ID when null", func(t *testing.T) {
147+
result := NullPropagation(Outline{Type: IntersectionArrowIteratorType, ID: 9, SubOutlines: []Outline{null, live}})
148+
require.Equal(t, NullIteratorType, result.Type)
149+
require.Equal(t, OutlineNodeID(9), result.ID)
150+
})
151+
152+
// --- ExclusionIteratorType ---
153+
154+
t.Run("exclusion: null left child (main set) → null", func(t *testing.T) {
155+
result := NullPropagation(Outline{Type: ExclusionIteratorType, SubOutlines: []Outline{null, live}})
156+
require.Equal(t, NullIteratorType, result.Type)
157+
})
158+
159+
t.Run("exclusion: null right child (excluded set) → unchanged (A − ∅ = A)", func(t *testing.T) {
160+
// Subtracting the empty set is a no-op; the node stays for a later pass.
161+
result := NullPropagation(Outline{Type: ExclusionIteratorType, SubOutlines: []Outline{live, null}})
162+
require.Equal(t, ExclusionIteratorType, result.Type)
163+
})
164+
165+
t.Run("exclusion: both null → null (left child triggers)", func(t *testing.T) {
166+
result := NullPropagation(Outline{Type: ExclusionIteratorType, SubOutlines: []Outline{null, null}})
167+
require.Equal(t, NullIteratorType, result.Type)
168+
})
169+
170+
t.Run("exclusion: neither null → unchanged", func(t *testing.T) {
171+
result := NullPropagation(Outline{Type: ExclusionIteratorType, SubOutlines: []Outline{live, live}})
172+
require.Equal(t, ExclusionIteratorType, result.Type)
173+
})
174+
175+
t.Run("exclusion: wrong child count (1) → unchanged (defensive guard)", func(t *testing.T) {
176+
result := NullPropagation(Outline{Type: ExclusionIteratorType, SubOutlines: []Outline{null}})
177+
require.Equal(t, ExclusionIteratorType, result.Type)
178+
})
179+
180+
t.Run("exclusion: preserves ID when null", func(t *testing.T) {
181+
result := NullPropagation(Outline{Type: ExclusionIteratorType, ID: 5, SubOutlines: []Outline{null, live}})
182+
require.Equal(t, NullIteratorType, result.Type)
183+
require.Equal(t, OutlineNodeID(5), result.ID)
184+
})
185+
186+
// --- CaveatIteratorType ---
187+
188+
t.Run("caveat: null child → null", func(t *testing.T) {
189+
result := NullPropagation(Outline{Type: CaveatIteratorType, SubOutlines: []Outline{null}})
190+
require.Equal(t, NullIteratorType, result.Type)
191+
})
192+
193+
t.Run("caveat: live child → unchanged", func(t *testing.T) {
194+
result := NullPropagation(Outline{Type: CaveatIteratorType, SubOutlines: []Outline{live}})
195+
require.Equal(t, CaveatIteratorType, result.Type)
196+
})
197+
198+
t.Run("caveat: wrong child count (2) → unchanged (defensive guard)", func(t *testing.T) {
199+
result := NullPropagation(Outline{Type: CaveatIteratorType, SubOutlines: []Outline{null, null}})
200+
require.Equal(t, CaveatIteratorType, result.Type)
201+
})
202+
203+
t.Run("caveat: preserves ID when null", func(t *testing.T) {
204+
result := NullPropagation(Outline{Type: CaveatIteratorType, ID: 3, SubOutlines: []Outline{null}})
205+
require.Equal(t, NullIteratorType, result.Type)
206+
require.Equal(t, OutlineNodeID(3), result.ID)
207+
})
208+
209+
// --- AliasIteratorType ---
210+
211+
t.Run("alias: null child → null", func(t *testing.T) {
212+
result := NullPropagation(Outline{Type: AliasIteratorType, SubOutlines: []Outline{null}})
213+
require.Equal(t, NullIteratorType, result.Type)
214+
})
215+
216+
t.Run("alias: live child → unchanged", func(t *testing.T) {
217+
result := NullPropagation(Outline{Type: AliasIteratorType, SubOutlines: []Outline{live}})
218+
require.Equal(t, AliasIteratorType, result.Type)
219+
})
220+
221+
t.Run("alias: wrong child count (2) → unchanged (defensive guard)", func(t *testing.T) {
222+
result := NullPropagation(Outline{Type: AliasIteratorType, SubOutlines: []Outline{null, null}})
223+
require.Equal(t, AliasIteratorType, result.Type)
224+
})
225+
226+
// --- RecursiveIteratorType ---
227+
228+
t.Run("recursive: null child → null", func(t *testing.T) {
229+
result := NullPropagation(Outline{Type: RecursiveIteratorType, SubOutlines: []Outline{null}})
230+
require.Equal(t, NullIteratorType, result.Type)
231+
})
232+
233+
t.Run("recursive: live child → unchanged", func(t *testing.T) {
234+
result := NullPropagation(Outline{Type: RecursiveIteratorType, SubOutlines: []Outline{live}})
235+
require.Equal(t, RecursiveIteratorType, result.Type)
236+
})
237+
238+
t.Run("recursive: wrong child count (2) → unchanged (defensive guard)", func(t *testing.T) {
239+
result := NullPropagation(Outline{Type: RecursiveIteratorType, SubOutlines: []Outline{null, null}})
240+
require.Equal(t, RecursiveIteratorType, result.Type)
241+
})
242+
243+
// --- Leaf / unhandled types → always unchanged ---
244+
245+
t.Run("null node itself → unchanged", func(t *testing.T) {
246+
result := NullPropagation(Outline{Type: NullIteratorType})
247+
require.Equal(t, NullIteratorType, result.Type)
248+
})
249+
250+
t.Run("datastore leaf → unchanged", func(t *testing.T) {
251+
result := NullPropagation(Outline{Type: DatastoreIteratorType})
252+
require.Equal(t, DatastoreIteratorType, result.Type)
253+
})
254+
255+
t.Run("self leaf → unchanged", func(t *testing.T) {
256+
result := NullPropagation(Outline{Type: SelfIteratorType})
257+
require.Equal(t, SelfIteratorType, result.Type)
258+
})
259+
260+
t.Run("fixed leaf → unchanged", func(t *testing.T) {
261+
result := NullPropagation(Outline{Type: FixedIteratorType})
262+
require.Equal(t, FixedIteratorType, result.Type)
263+
})
264+
265+
t.Run("recursive sentinel leaf → unchanged", func(t *testing.T) {
266+
result := NullPropagation(Outline{Type: RecursiveSentinelIteratorType})
267+
require.Equal(t, RecursiveSentinelIteratorType, result.Type)
268+
})
269+
}
270+
9271
func TestReorderMutation(t *testing.T) {
10272
t.Run("reorders children correctly", func(t *testing.T) {
11273
child0 := Outline{Type: FixedIteratorType, Args: &IteratorArgs{FixedPaths: []Path{*MustPathFromString("document:doc0#viewer@user:alice")}}}

pkg/query/queryopt/caveat_pushdown.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ func init() {
99
Pushes caveat evalution to the lowest point in the tree.
1010
Cannot push through intersection arrows
1111
`,
12+
Priority: 20,
1213
NewTransform: func(_ RequestParams) OutlineTransform {
1314
return func(outline query.Outline) query.Outline {
1415
return query.MutateOutline(outline, []query.OutlineMutation{caveatPushdown})

pkg/query/queryopt/reachability_pruning.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ func init() {
1111
Replaces subtrees with NullIteratorType nodes when they can never
1212
produce the target subject type of the request.
1313
`,
14+
Priority: 0,
1415
NewTransform: func(params RequestParams) OutlineTransform {
1516
return reachabilityPruning(params)
1617
},

pkg/query/queryopt/registry.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type RequestParams struct {
5858
func OptimizersForRequest(params RequestParams) []Optimizer {
5959
base := []Optimizer{
6060
optimizationRegistry["simple-caveat-pushdown"],
61+
optimizationRegistry["set-simplification"],
6162
optimizationRegistry["reachability-pruning"],
6263
}
6364

0 commit comments

Comments
 (0)