Skip to content

Conversation

@barakmich
Copy link
Contributor

Adds a pattern and a few basic static optimizers to pkg/query to rewrite query plans.

Fixes a deduplication bug when we elide Unions (we might be able to further remove deduplication from unions, tbd)

Also tests both optimized and unoptimized trees for correctness in integration tests.

@barakmich barakmich requested a review from a team as a code owner October 29, 2025 22:25
@github-actions github-actions bot added the area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools) label Oct 29, 2025
@codecov
Copy link

codecov bot commented Oct 29, 2025

Codecov Report

❌ Patch coverage is 76.02740% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.10%. Comparing base (e55404b) to head (d8d75fb).

Files with missing lines Patch % Lines
pkg/query/optimize.go 77.15% 4 Missing and 4 partials ⚠️
pkg/query/optimize_caveat.go 77.78% 4 Missing and 4 partials ⚠️
pkg/query/path.go 61.91% 5 Missing and 3 partials ⚠️
pkg/query/union.go 56.25% 5 Missing and 2 partials ⚠️
pkg/query/optimize_simple.go 88.24% 2 Missing and 2 partials ⚠️

❌ Your project check has failed because the head coverage (59.10%) is below the target coverage (75.00%). You can increase the head coverage or adjust the target coverage.

❗ There is a different number of reports uploaded between BASE (e55404b) and HEAD (d8d75fb). Click for more details.

HEAD has 25 uploads less than BASE
Flag BASE (e55404b) HEAD (d8d75fb)
51 26
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #2669       +/-   ##
===========================================
- Coverage   79.46%   59.10%   -20.35%     
===========================================
  Files         455      416       -39     
  Lines       47161    43848     -3313     
===========================================
- Hits        37470    25911    -11559     
- Misses       6945    15438     +8493     
+ Partials     2746     2499      -247     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@barakmich barakmich force-pushed the barakmich/optimizers branch from a7608e1 to c94ede6 Compare October 29, 2025 22:48
@barakmich barakmich force-pushed the barakmich/optimizers branch from c94ede6 to d8d75fb Compare October 30, 2025 23:06
{"unoptimized", false},
{"optimized", true},
} {
optimizationMode := optimizationMode
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, having to copy the variable hasn't been necessary since go 1.22 https://go.dev/wiki/LoopvarExperiment

require.Equal(t, fixed2, result2)
})

t.Run("optimizer order independence - union with all empty", func(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI this test is failing

optimize_test.go:467: 
        	Error Trace:	/Users/miparnisari/Documents/GitHub/spicedb/pkg/query/optimize_test.go:467
        	Error:      	Should be true
        	Test:       	TestApplyOptimizations/optimizer_order_independence_-_union_with_all_empty

// to any iterator tree without needing runtime information or context.
var StaticOptimizations = []OptimizerFunc{
RemoveNullIterators,
ElideSingletonUnionAndIntersection,
Copy link
Contributor

@miparnisari miparnisari Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to google what "elide" meant 😅 Now that I looked at the code I would have called this "collapse". But i'm not going to die on this hill

Comment on lines +142 to +146
union := NewUnion()
fixed := newNonEmptyFixedIterator()
empty := NewEmptyFixedIterator()
union.addSubIterator(fixed)
union.addSubIterator(empty)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for a follow-up PR: this would be slightly easier to read if we could do

fixed := newNonEmptyFixedIterator()
empty := NewEmptyFixedIterator()
union := NewUnion(fixed, empty)

require.Equal(t, fixed2, resultUnion.subIts[1])
})

t.Run("replaces intersection with empty if it contains empty fixed", func(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
t.Run("replaces intersection with empty if it contains empty fixed", func(t *testing.T) {
t.Run("replaces intersection with empty if it contains empty and fixed", func(t *testing.T) {

Comment on lines +37 to +39
// If all subiterators were empty, return empty
if len(newSubs) == 0 {
return NewEmptyFixedIterator(), true, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code path isn't covered with unit tests

func TestApplyOptimizations(t *testing.T) {
t.Parallel()

t.Run("applies optimization to iterator", func(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 this same test is in line 73

Comment on lines +306 to +319
// Create two different optimizers
unionOptimizer := func(it Iterator) (Iterator, bool, error) {
if u, ok := it.(*Union); ok && len(u.subIts) == 1 {
return u.subIts[0], true, nil
}
return it, false, nil
}

intersectionOptimizer := func(it Iterator) (Iterator, bool, error) {
if i, ok := it.(*Intersection); ok && len(i.subIts) == 1 {
return i.subIts[0], true, nil
}
return it, false, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 these are the ElideSingletonUnionAndIntersection

require.NoError(t, err)
require.True(t, changed1)

// Create the same structure again
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why? ApplyOptimizations doesn't mutate the inputs so you can reuse it

// has a caveat that matches the given caveat name.
func relationContainsCaveat(rel *RelationIterator, caveat *core.ContextualizedCaveat) bool {
if rel.base == nil || caveat == nil {
return false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code path isn't covered with unit tests

Copy link
Contributor

@miparnisari miparnisari left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice 😄

There is one test that fails both locally and in CI, and another one that fails only locally. Are we using --failfast in CI?

$ go test -race . -count=1
# github.com/authzed/spicedb/pkg/query.test
ld: warning: '/private/var/folders/5p/328y159n1334j9czdhzl2ts40000gn/T/go-link-2977391469/000013.o' has malformed LC_DYSYMTAB, expected 98 undefined symbols to start at index 1626, found 95 undefined symbols starting at index 1626
--- FAIL: TestIteratorTracing (0.00s)
    --- FAIL: TestIteratorTracing/Union_tracing (0.00s)
        tracing_test.go:101: 
                Error Trace:    /Users/miparnisari/Documents/GitHub/spicedb/pkg/query/tracing_test.go:101
                Error:          Should be true
                Test:           TestIteratorTracing/Union_tracing
--- FAIL: TestAliasIterator (0.03s)
    alias_test.go:47: 
                Error Trace:    /Users/miparnisari/Documents/GitHub/spicedb/pkg/query/alias_test.go:47
                Error:          "[{{doc1 document} read user:alice#... <nil> <nil> [] map[]}]" should have 3 item(s), but has 1
                Test:           TestAliasIterator
                Messages:       should have 3 rewritten relations
    --- FAIL: TestAliasIterator/Check_BasicRelationRewriting (0.00s)
panic: test executed panic(nil) or runtime.Goexit

goroutine 214 [running]:
testing.tRunner.func1.2({0x105a5c240, 0x1066a1b40})
        /opt/homebrew/opt/go/libexec/src/testing/testing.go:1872 +0x2b4
testing.tRunner.func1()
        /opt/homebrew/opt/go/libexec/src/testing/testing.go:1875 +0x460
runtime.Goexit()
        /opt/homebrew/opt/go/libexec/src/runtime/panic.go:615 +0x60
testing.(*common).FailNow(0xc000102fc0)
        /opt/homebrew/opt/go/libexec/src/testing/testing.go:1013 +0x70
github.com/stretchr/testify/require.Len({0x105c755e8, 0xc000102fc0}, {0x1059d5200, 0xc000948c48}, 0x3, {0xc000659e68, 0x1, 0x1})
        /Users/miparnisari/go/pkg/mod/github.com/stretchr/testify@v1.11.1/require/require.go:1203 +0xe4
github.com/stretchr/testify/require.(*Assertions).Len(0xc0006cc1d0, {0x1059d5200, 0xc000948c48}, 0x3, {0xc000659e68, 0x1, 0x1})
        /Users/miparnisari/go/pkg/mod/github.com/stretchr/testify@v1.11.1/require/require_forward.go:954 +0xac
github.com/authzed/spicedb/pkg/query.TestAliasIterator.func1(0xc000780e00)
        /Users/miparnisari/Documents/GitHub/spicedb/pkg/query/alias_test.go:47 +0x79c
testing.tRunner(0xc000780e00, 0xc000904348)
        /opt/homebrew/opt/go/libexec/src/testing/testing.go:1934 +0x168
created by testing.(*T).Run in goroutine 9
        /opt/homebrew/opt/go/libexec/src/testing/testing.go:1997 +0x6e4
FAIL    github.com/authzed/spicedb/pkg/query    0.626s
FAIL
[15:03:13] ~/Documents/GitHub/spicedb/pkg/query (barakmich/optimizers) $ 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants