Skip to content

Commit 9379697

Browse files
authored
Merge pull request #2376 from josephschorr/crdb-index-logic
Improve CRDB index forcing logic to be more fine-grain
2 parents cc3b059 + 70d9f66 commit 9379697

File tree

3 files changed

+183
-13
lines changed

3 files changed

+183
-13
lines changed

internal/datastore/crdb/readwrite.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,10 @@ func (rwt *crdbReadWriteTXN) insertQuery() sq.InsertBuilder {
130130
return psql.Insert(rwt.schema.RelationshipTableName)
131131
}
132132

133-
func (rwt *crdbReadWriteTXN) queryDeleteTuples(index common.IndexDefinition) sq.DeleteBuilder {
133+
func (rwt *crdbReadWriteTXN) queryDeleteTuples(index *common.IndexDefinition) sq.DeleteBuilder {
134+
if index == nil {
135+
return psql.Delete(rwt.schema.RelationshipTableName)
136+
}
134137
return psql.Delete(rwt.schema.RelationshipTableName + "@" + index.Name)
135138
}
136139

@@ -281,7 +284,7 @@ func (rwt *crdbReadWriteTXN) WriteRelationships(ctx context.Context, mutations [
281284
bulkTouch := rwt.queryTouchTuple()
282285
var bulkTouchCount int64
283286

284-
bulkDelete := rwt.queryDeleteTuples(schema.IndexPrimaryKey)
287+
bulkDelete := rwt.queryDeleteTuples(nil)
285288
bulkDeleteOr := sq.Or{}
286289
var bulkDeleteCount int64
287290

@@ -533,7 +536,7 @@ func (rwt *crdbReadWriteTXN) DeleteNamespaces(ctx context.Context, nsNames ...st
533536
return fmt.Errorf(errUnableToDeleteConfig, err)
534537
}
535538

536-
deleteTupleSQL, deleteTupleArgs, err := rwt.queryDeleteTuples(schema.IndexPrimaryKey).Where(sq.Or(tplClauses)).ToSql()
539+
deleteTupleSQL, deleteTupleArgs, err := rwt.queryDeleteTuples(nil).Where(sq.Or(tplClauses)).ToSql()
537540
if err != nil {
538541
return fmt.Errorf(errUnableToDeleteConfig, err)
539542
}

internal/datastore/crdb/schema/indexes.go

+42-10
Original file line numberDiff line numberDiff line change
@@ -86,20 +86,52 @@ func IndexingHintForQueryShape(schema common.SchemaInformation, qs queryshape.Sh
8686
}
8787
}
8888

89-
// IndexForFilter returns the index to use for a given relationships filter.
90-
func IndexForFilter(schema common.SchemaInformation, filter datastore.RelationshipsFilter) common.IndexDefinition {
91-
if schema.IntegrityEnabled {
92-
// Don't force anything since we don't have the other indexes.
93-
return IndexPrimaryKey
89+
// IndexForFilter returns the index to use for a given relationships filter or nil if no index is forced.
90+
func IndexForFilter(schema common.SchemaInformation, filter datastore.RelationshipsFilter) *common.IndexDefinition {
91+
resourceFieldDepth := 0
92+
if filter.OptionalResourceType != "" {
93+
resourceFieldDepth = 1
94+
if len(filter.OptionalResourceIds) > 0 || filter.OptionalResourceIDPrefix != "" {
95+
resourceFieldDepth = 2
96+
if filter.OptionalResourceRelation != "" {
97+
resourceFieldDepth = 3
98+
}
99+
}
100+
}
101+
102+
subjectFieldDepth := 0
103+
for _, subjectSelector := range filter.OptionalSubjectsSelectors {
104+
sfd := 0
105+
if len(subjectSelector.OptionalSubjectIds) > 0 {
106+
sfd = 1
107+
if subjectSelector.OptionalSubjectType != "" {
108+
sfd = 2
109+
if subjectSelector.RelationFilter.NonEllipsisRelation != "" {
110+
sfd = 3
111+
}
112+
}
113+
}
114+
subjectFieldDepth = max(subjectFieldDepth, sfd)
115+
}
116+
117+
if resourceFieldDepth == 0 && subjectFieldDepth == 0 {
118+
return nil
94119
}
95120

96-
// If there is no resource type specified, but there is a subject filter, use the
97-
// inverse index.
98-
if filter.OptionalResourceType == "" && len(filter.OptionalSubjectsSelectors) > 0 {
99-
return IndexRelationshipBySubject
121+
if resourceFieldDepth > subjectFieldDepth {
122+
return &IndexPrimaryKey
123+
}
124+
125+
if resourceFieldDepth < subjectFieldDepth {
126+
if schema.IntegrityEnabled {
127+
// Don't force this index since it doesn't exist for integrity-enabled schemas.
128+
return nil
129+
}
130+
131+
return &IndexRelationshipBySubject
100132
}
101133

102-
return IndexPrimaryKey
134+
return nil
103135
}
104136

105137
// forcedIndex is an index hint that forces the use of a specific index.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package schema
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/authzed/spicedb/internal/datastore/common"
9+
"github.com/authzed/spicedb/pkg/datastore"
10+
)
11+
12+
func TestIndexForFilter(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
filter datastore.RelationshipsFilter
16+
expected string
17+
}{
18+
{
19+
name: "no filter",
20+
filter: datastore.RelationshipsFilter{},
21+
expected: "",
22+
},
23+
{
24+
name: "filter by resource type",
25+
filter: datastore.RelationshipsFilter{OptionalResourceType: "foo"},
26+
expected: "pk_relation_tuple",
27+
},
28+
{
29+
name: "filter by resource type and relation",
30+
filter: datastore.RelationshipsFilter{
31+
OptionalResourceType: "foo",
32+
OptionalResourceRelation: "bar",
33+
},
34+
expected: "pk_relation_tuple",
35+
},
36+
{
37+
name: "filter by resource type, resource ID and relation",
38+
filter: datastore.RelationshipsFilter{
39+
OptionalResourceType: "foo",
40+
OptionalResourceIds: []string{"baz"},
41+
OptionalResourceRelation: "bar",
42+
},
43+
expected: "pk_relation_tuple",
44+
},
45+
{
46+
name: "filter by subject type, subject ID and relation",
47+
filter: datastore.RelationshipsFilter{
48+
OptionalSubjectsSelectors: []datastore.SubjectsSelector{
49+
{
50+
OptionalSubjectType: "foo",
51+
OptionalSubjectIds: []string{"bar"},
52+
RelationFilter: datastore.SubjectRelationFilter{
53+
NonEllipsisRelation: "baz",
54+
},
55+
},
56+
},
57+
},
58+
expected: "ix_relation_tuple_by_subject",
59+
},
60+
{
61+
name: "filter by subject type, subject ID",
62+
filter: datastore.RelationshipsFilter{
63+
OptionalSubjectsSelectors: []datastore.SubjectsSelector{
64+
{
65+
OptionalSubjectType: "foo",
66+
OptionalSubjectIds: []string{"bar"},
67+
},
68+
},
69+
},
70+
expected: "ix_relation_tuple_by_subject",
71+
},
72+
{
73+
name: "filter by subject relation, subject ID",
74+
filter: datastore.RelationshipsFilter{
75+
OptionalSubjectsSelectors: []datastore.SubjectsSelector{
76+
{
77+
OptionalSubjectIds: []string{"bar"},
78+
RelationFilter: datastore.SubjectRelationFilter{
79+
NonEllipsisRelation: "baz",
80+
},
81+
},
82+
},
83+
},
84+
expected: "ix_relation_tuple_by_subject",
85+
},
86+
{
87+
name: "filter by subject type",
88+
filter: datastore.RelationshipsFilter{
89+
OptionalSubjectsSelectors: []datastore.SubjectsSelector{
90+
{
91+
OptionalSubjectType: "foo",
92+
},
93+
},
94+
},
95+
expected: "",
96+
},
97+
{
98+
name: "filter by resource type and subject type",
99+
filter: datastore.RelationshipsFilter{
100+
OptionalResourceType: "foo",
101+
OptionalSubjectsSelectors: []datastore.SubjectsSelector{
102+
{
103+
OptionalSubjectType: "foo",
104+
},
105+
},
106+
},
107+
expected: "pk_relation_tuple",
108+
},
109+
{
110+
name: "filter by resource type and subject object ID",
111+
filter: datastore.RelationshipsFilter{
112+
OptionalResourceType: "foo",
113+
OptionalSubjectsSelectors: []datastore.SubjectsSelector{
114+
{
115+
OptionalSubjectIds: []string{"bar"},
116+
},
117+
},
118+
},
119+
expected: "",
120+
},
121+
}
122+
123+
schema := Schema(common.ColumnOptimizationOptionNone, false, false)
124+
for _, test := range tests {
125+
t.Run(test.name, func(t *testing.T) {
126+
index := IndexForFilter(*schema, test.filter)
127+
if test.expected == "" {
128+
require.Nil(t, index)
129+
} else {
130+
require.NotNil(t, index)
131+
require.Equal(t, test.expected, index.Name)
132+
}
133+
})
134+
}
135+
}

0 commit comments

Comments
 (0)