Skip to content

Implement a typechecking function in schema #2374

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions pkg/schema/type_check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package schema

import (
"context"
"fmt"

"github.com/authzed/spicedb/pkg/genutil/mapz"
corev1 "github.com/authzed/spicedb/pkg/proto/core/v1"
)

const ellipsesRelation = "..."

// GetRecursiveTypesForRelation returns, for a given definition and relation, are the potential
// subject definition names of that relation.
func (ts *TypeSystem) GetRecursiveTypesForRelation(ctx context.Context, defName string, relationName string) ([]string, error) {
seen := mapz.NewSet[string]()
set, err := ts.getTypesForRelationInternal(ctx, defName, relationName, seen, false)
if err != nil {
return nil, err
}
return set.AsSlice(), nil
}

// GetRecursiveSubtypesForRelation returns, for a given definition and relation, are the potential
// subject definition names of that relation, as well as any relation subtypes (eg, `group#member`) that may occur.
func (ts *TypeSystem) GetRecursiveSubtypesForRelation(ctx context.Context, defName string, relationName string) ([]string, error) {
seen := mapz.NewSet[string]()
set, err := ts.getTypesForRelationInternal(ctx, defName, relationName, seen, true)
if err != nil {
return nil, err
}
return set.AsSlice(), nil
}

func (ts *TypeSystem) getTypesForRelationInternal(ctx context.Context, defName string, relationName string, seen *mapz.Set[string], addRelations bool) (*mapz.Set[string], error) {
id := fmt.Sprint(defName, "#", relationName)
if seen.Has(id) {
return nil, nil
}
seen.Add(id)
def, err := ts.GetDefinition(ctx, defName)
if err != nil {
return nil, err
}
rel, ok := def.GetRelation(relationName)
Copy link
Member

Choose a reason for hiding this comment

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

newlines after if statements

Copy link
Contributor

@miparnisari miparnisari May 5, 2025

Choose a reason for hiding this comment

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

The wsl linter does this for you 😄 https://github.com/bombsimon/wsl.

We should send a separate PR adding it

if !ok {
return nil, asTypeError(NewRelationNotFoundErr(defName, relationName))
}
if rel.TypeInformation != nil {
return ts.getTypesForInfo(ctx, defName, rel.TypeInformation, seen, addRelations)
} else if rel.UsersetRewrite != nil {
Copy link
Member

Choose a reason for hiding this comment

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

drop the else here, since all branches return

return ts.getTypesForRewrite(ctx, defName, rel.UsersetRewrite, seen, addRelations)
}
return nil, asTypeError(NewMissingAllowedRelationsErr(defName, relationName))

Check warning on line 54 in pkg/schema/type_check.go

View check run for this annotation

Codecov / codecov/patch

pkg/schema/type_check.go#L54

Added line #L54 was not covered by tests
}

func (ts *TypeSystem) getTypesForInfo(ctx context.Context, defName string, rel *corev1.TypeInformation, seen *mapz.Set[string], addRelations bool) (*mapz.Set[string], error) {
out := mapz.NewSet[string]()
for _, dr := range rel.GetAllowedDirectRelations() {
if dr.GetRelation() == ellipsesRelation {
out.Add(dr.GetNamespace())
} else if dr.GetRelation() != "" {
if addRelations {
out.Add(fmt.Sprintf("%s#%s", dr.GetNamespace(), dr.GetRelation()))
}
rest, err := ts.getTypesForRelationInternal(ctx, dr.GetNamespace(), dr.GetRelation(), seen, addRelations)
if err != nil {
return nil, err
}

Check warning on line 69 in pkg/schema/type_check.go

View check run for this annotation

Codecov / codecov/patch

pkg/schema/type_check.go#L68-L69

Added lines #L68 - L69 were not covered by tests
out.Merge(rest)
} else {
// It's a wildcard, so all things of that type count
out.Add(dr.GetNamespace())
}
}
return out, nil
}

func (ts *TypeSystem) getTypesForRewrite(ctx context.Context, defName string, rel *corev1.UsersetRewrite, seen *mapz.Set[string], addRelations bool) (*mapz.Set[string], error) {
out := mapz.NewSet[string]()

// We're finding the union of all the things touched, regardless.
toCheck := []*corev1.SetOperation{rel.GetUnion(), rel.GetIntersection(), rel.GetExclusion()}

for _, op := range toCheck {
if op == nil {
continue
}
for _, child := range op.GetChild() {
if computed := child.GetComputedUserset(); computed != nil {
set, err := ts.getTypesForRelationInternal(ctx, defName, computed.GetRelation(), seen, addRelations)
if err != nil {
return nil, err
}

Check warning on line 94 in pkg/schema/type_check.go

View check run for this annotation

Codecov / codecov/patch

pkg/schema/type_check.go#L93-L94

Added lines #L93 - L94 were not covered by tests
out.Merge(set)
}
if rewrite := child.GetUsersetRewrite(); rewrite != nil {
sub, err := ts.getTypesForRewrite(ctx, defName, rewrite, seen, addRelations)
if err != nil {
return nil, err
}

Check warning on line 101 in pkg/schema/type_check.go

View check run for this annotation

Codecov / codecov/patch

pkg/schema/type_check.go#L100-L101

Added lines #L100 - L101 were not covered by tests
out.Merge(sub)
}
if userset := child.GetTupleToUserset(); userset != nil {
set, err := ts.getTypesForRelationInternal(ctx, defName, userset.GetTupleset().GetRelation(), seen, addRelations)
if err != nil {
return nil, err
}

Check warning on line 108 in pkg/schema/type_check.go

View check run for this annotation

Codecov / codecov/patch

pkg/schema/type_check.go#L107-L108

Added lines #L107 - L108 were not covered by tests
if set == nil {
// We've already seen it.
continue
}
for _, s := range set.AsSlice() {
targets, err := ts.getTypesForRelationInternal(ctx, s, userset.GetComputedUserset().GetRelation(), seen, addRelations)
if err != nil {
return nil, err
}
out.Merge(targets)
}
}
if functioned := child.GetFunctionedTupleToUserset(); functioned != nil {
set, err := ts.getTypesForRelationInternal(ctx, defName, functioned.GetTupleset().GetRelation(), seen, addRelations)
if err != nil {
return nil, err
}

Check warning on line 125 in pkg/schema/type_check.go

View check run for this annotation

Codecov / codecov/patch

pkg/schema/type_check.go#L124-L125

Added lines #L124 - L125 were not covered by tests
if set == nil {
// We've already seen it.
continue

Check warning on line 128 in pkg/schema/type_check.go

View check run for this annotation

Codecov / codecov/patch

pkg/schema/type_check.go#L127-L128

Added lines #L127 - L128 were not covered by tests
}
for _, s := range set.AsSlice() {
targets, err := ts.getTypesForRelationInternal(ctx, s, functioned.GetComputedUserset().GetRelation(), seen, addRelations)
if targets == nil {
continue

Check warning on line 133 in pkg/schema/type_check.go

View check run for this annotation

Codecov / codecov/patch

pkg/schema/type_check.go#L133

Added line #L133 was not covered by tests
}
if err != nil {
return nil, err
}

Check warning on line 137 in pkg/schema/type_check.go

View check run for this annotation

Codecov / codecov/patch

pkg/schema/type_check.go#L136-L137

Added lines #L136 - L137 were not covered by tests
out.Merge(targets)
}
}
}
}
return out, nil
}
Loading
Loading