Skip to content

Commit

Permalink
Add @type directive to enforce type constraints. (hypermodeinc#3003)
Browse files Browse the repository at this point in the history
Predicates in queries can now have a @type predicate that enforces
that all the given results are of the given type. For now this is
just the equivalent of filtering using the existing type function but
later more checks will be added.
  • Loading branch information
martinmr authored and dna2github committed Jul 19, 2019
1 parent 7dbbe84 commit 0eb2b2d
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 0 deletions.
39 changes: 39 additions & 0 deletions gql/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -1880,6 +1880,40 @@ func parseGroupby(it *lex.ItemIterator, gq *GraphQuery) error {
return nil
}

func parseType(it *lex.ItemIterator, gq *GraphQuery) error {
it.Next()
if it.Item().Typ != itemLeftRound {
return it.Item().Errorf("Expected a left round after type")
}

it.Next()
if it.Item().Typ != itemName {
return it.Item().Errorf("Expected a type name inside type directive")
}
typeName := it.Item().Val

it.Next()
if it.Item().Typ != itemRightRound {
return it.Item().Errorf("Expected ) after the type name in type directive")
}

// For now @type(TypeName) is equivalent of filtering using the type function.
// Later the type declarations will be used to ensure that the fields inside
// each block correspond to the specified type.
gq.Filter = &FilterTree{
Func: &Function{
Name: "type",
Args: []Arg{
Arg{
Value: typeName,
},
},
},
}

return nil
}

// parseFilter parses the filter directive to produce a QueryFilter / parse tree.
func parseFilter(it *lex.ItemIterator) (*FilterTree, error) {
it.Next()
Expand Down Expand Up @@ -2110,6 +2144,11 @@ func parseDirective(it *lex.ItemIterator, curp *GraphQuery) error {
}
curp.IsGroupby = true
parseGroupby(it, curp)
case "type":
err := parseType(it, curp)
if err != nil {
return err
}
default:
return item.Errorf("Unknown directive [%s]", item.Val)
}
Expand Down
78 changes: 78 additions & 0 deletions gql/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4527,3 +4527,81 @@ func TestTypeInFilter(t *testing.T) {
require.Equal(t, 1, len(gq.Query[0].Filter.Func.Args))
require.Equal(t, "Person", gq.Query[0].Filter.Func.Args[0].Value)
}

func TestTypeFilterInPredicate(t *testing.T) {
q := `
query {
me(func: uid(0x01)) {
friend @filter(type(Person)) {
name
}
}
}`
gq, err := Parse(Request{Str: q})
require.NoError(t, err)
require.Equal(t, 1, len(gq.Query))
require.Equal(t, "uid", gq.Query[0].Func.Name)
require.Equal(t, 1, len(gq.Query[0].Children))
require.Equal(t, "friend", gq.Query[0].Children[0].Attr)

require.Equal(t, "type", gq.Query[0].Children[0].Filter.Func.Name)
require.Equal(t, 1, len(gq.Query[0].Children[0].Filter.Func.Args))
require.Equal(t, "Person", gq.Query[0].Children[0].Filter.Func.Args[0].Value)

require.Equal(t, 1, len(gq.Query[0].Children[0].Children))
require.Equal(t, "name", gq.Query[0].Children[0].Children[0].Attr)
}

func TestTypeInPredicate(t *testing.T) {
q := `
query {
me(func: uid(0x01)) {
friend @type(Person) {
name
}
}
}`
gq, err := Parse(Request{Str: q})
require.NoError(t, err)
require.Equal(t, 1, len(gq.Query))
require.Equal(t, "uid", gq.Query[0].Func.Name)
require.Equal(t, 1, len(gq.Query[0].Children))
require.Equal(t, "friend", gq.Query[0].Children[0].Attr)

require.Equal(t, "type", gq.Query[0].Children[0].Filter.Func.Name)
require.Equal(t, 1, len(gq.Query[0].Children[0].Filter.Func.Args))
require.Equal(t, "Person", gq.Query[0].Children[0].Filter.Func.Args[0].Value)

require.Equal(t, 1, len(gq.Query[0].Children[0].Children))
require.Equal(t, "name", gq.Query[0].Children[0].Children[0].Attr)
}

func TestMultipleTypeDirectives(t *testing.T) {
q := `
query {
me(func: uid(0x01)) {
friend @type(Person) {
pet @type(Animal) {
name
}
}
}
}`
gq, err := Parse(Request{Str: q})
require.NoError(t, err)
require.Equal(t, 1, len(gq.Query))
require.Equal(t, "uid", gq.Query[0].Func.Name)
require.Equal(t, 1, len(gq.Query[0].Children))
require.Equal(t, "friend", gq.Query[0].Children[0].Attr)

require.Equal(t, "type", gq.Query[0].Children[0].Filter.Func.Name)
require.Equal(t, 1, len(gq.Query[0].Children[0].Filter.Func.Args))
require.Equal(t, "Person", gq.Query[0].Children[0].Filter.Func.Args[0].Value)

require.Equal(t, 1, len(gq.Query[0].Children[0].Children))
require.Equal(t, "pet", gq.Query[0].Children[0].Children[0].Attr)

require.Equal(t, "type", gq.Query[0].Children[0].Children[0].Filter.Func.Name)
require.Equal(t, 1, len(gq.Query[0].Children[0].Children[0].Filter.Func.Args))
require.Equal(t, "Animal", gq.Query[0].Children[0].Children[0].Filter.Func.Args[0].Value)
}
16 changes: 16 additions & 0 deletions query/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ symbol : string @index(exact) .
room : string @index(term) .
office.room : [uid] .
best_friend : uid .
pet : [uid] .
`

func populateCluster(t *testing.T) {
Expand All @@ -211,6 +212,12 @@ func populateCluster(t *testing.T) {

addTriplesToCluster(t, `
<1> <name> "Michonne" .
<2> <name> "King Lear" .
<3> <name> "Margaret" .
<4> <name> "Leonard" .
<5> <name> "Garfield" .
<6> <name> "Bear" .
<7> <name> "Nemo" .
<23> <name> "Rick Grimes" .
<24> <name> "Glenn Rhee" .
<25> <name> "Daryl Dixon" .
Expand Down Expand Up @@ -425,6 +432,15 @@ func populateCluster(t *testing.T) {
<2> <type> "Person" .
<3> <type> "Person" .
<4> <type> "Person" .
<5> <type> "Animal" .
<6> <type> "Animal" .
<2> <pet> <5> .
<3> <pet> <6> .
<4> <pet> <7> .
<2> <enemy> <3> .
<2> <enemy> <4> .
`)

addGeoPointToCluster(t, 1, "loc", []float64{1.1, 2.0})
Expand Down
31 changes: 31 additions & 0 deletions query/query3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1913,3 +1913,34 @@ func TestTypeFilterUnknownType(t *testing.T) {
js := processQueryNoErr(t, query)
require.JSONEq(t, `{"data": {"me":[]}}`, js)
}

func TestTypeDirectiveInPredicate(t *testing.T) {
query := `
{
me(func: uid(0x2)) {
enemy @type(Person) {
name
}
}
}
`
js := processQueryNoErr(t, query)
require.JSONEq(t, `{"data": {"me":[{"enemy":[{"name":"Margaret"}, {"name":"Leonard"}]}]}}`, js)
}

func TestMultipleTypeDirectivesInPredicate(t *testing.T) {
query := `
{
me(func: uid(0x2)) {
enemy @type(Person) {
name
pet @type(Animal) {
name
}
}
}
}
`
js := processQueryNoErr(t, query)
require.JSONEq(t, `{"data": {"me":[{"enemy":[{"name":"Margaret", "pet":[{"name":"Bear"}]}, {"name":"Leonard"}]}]}}`, js)
}

0 comments on commit 0eb2b2d

Please sign in to comment.