diff --git a/gql/parser_mutation.go b/gql/parser_mutation.go index 50366e1f0d0..8439974a8fc 100644 --- a/gql/parser_mutation.go +++ b/gql/parser_mutation.go @@ -17,27 +17,43 @@ package gql import ( - "errors" - "github.com/dgraph-io/dgo/protos/api" "github.com/dgraph-io/dgraph/lex" "github.com/dgraph-io/dgraph/x" ) -func ParseMutation(mutation string) (*api.Mutation, error) { +// ParseMutationBlock parses a block of text into a mutation. +// Returns an object with a mutation or a transaction query +// with mutation, otherwise returns nil with an error. +func ParseMutationBlock(mutation string) (*api.Mutation, error) { lexer := lex.NewLexer(mutation) - lexer.Run(lexInsideMutation) + lexer.Run(lexIdentifyMutationBlock) it := lexer.NewIterator() - var mu api.Mutation if !it.Next() { - return nil, errors.New("Invalid mutation") + return nil, x.Errorf("Invalid mutation") } + item := it.Item() - if item.Typ != itemLeftCurl { + switch item.Typ { + // mutation upsert block + case itemMutationUpsert: + // parse query block + upsertQuery, err := parseMutationUpsertQuery(it) + if err != nil { + return nil, err + } + // parse mutation block + + // mutation block + case itemLeftCurl: + + default: return nil, x.Errorf("Expected { at the start of block. Got: [%s]", item.Val) } +} +func ParseMutation(it *lex.ItemIterator) (string, error) { for it.Next() { item := it.Item() if item.Typ == itemText { @@ -45,18 +61,59 @@ func ParseMutation(mutation string) (*api.Mutation, error) { } if item.Typ == itemRightCurl { // mutations must be enclosed in a single block. - if it.Next() && it.Item().Typ != lex.ItemEOF { + if !inTxn && it.Next() && it.Item().Typ != lex.ItemEOF { return nil, x.Errorf("Unexpected %s after the end of the block.", it.Item().Val) } + return &mu, nil } + if item.Typ == itemMutationOp { if err := parseMutationOp(it, item.Val, &mu); err != nil { return nil, err } } } - return nil, x.Errorf("Invalid mutation.") + return nil, x.Errorf("Invalid mutation") +} + +// parseMutationUpsertQuery gets the text inside a txn query block. It is possible that there's +// no query to be found, in that case it's the caller's responsibility to fail. +// Returns the query text if any is found, otherwise an empty string with error. +func parseMutationUpsertQuery(it *lex.ItemIterator) (string, error) { + var query string + var parse bool + for it.Next() { + item := it.Item() + switch item.Typ { + case itemLeftCurl: + continue + case itemMutationOpContent: + if !parse { + return "", x.Errorf("Invalid query block.") + } + query = item.Val + case itemMutationUpsertOp: + if item.Val == "query" { + if parse { + return "", x.Errorf("Too many query blocks in txn") + } + parse = true + continue + } + // TODO: mutation conditionals + if item.Val != "mutation" { + return "", x.Errorf("Invalid txn operator %q.", item.Val) + } + if !it.Next() { + return "", x.Errorf("Invalid mutation block") + } + return query, nil + default: + return "", x.Errorf("Unexpected %q inside of txn block.", item.Val) + } + } + return query, nil } // parseMutationOp parses and stores set or delete operation string in Mutation. @@ -73,7 +130,7 @@ func parseMutationOp(it *lex.ItemIterator, op string, mu *api.Mutation) error { } parse = true } - if item.Typ == itemMutationContent { + if item.Typ == itemMutationOpContent { if !parse { return x.Errorf("Mutation syntax invalid.") } diff --git a/gql/parser_upsert_test.go b/gql/parser_upsert_test.go new file mode 100644 index 00000000000..6f80cdd2748 --- /dev/null +++ b/gql/parser_upsert_test.go @@ -0,0 +1,260 @@ +/* + * Copyright 2017-2018 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gql + +import ( + "testing" + + "github.com/dgraph-io/dgo/protos/api" + "github.com/stretchr/testify/require" +) + +func TestParseMutationTxn1(t *testing.T) { + m := ` + txn { + query { + me(func: eq(email, "someone@gmail.com")) { + v as uid + } + } + + mutation { + set { + uid(v) "Some One" . + uid(v) "someone@gmail.com" . + } + } + }` + mu, err := ParseMutation(m) + require.NoError(t, err) + require.NotNil(t, mu) + query := `{ + me(func: eq(email, "someone@gmail.com")) { + v as uid + } + }` + require.EqualValues(t, query, mu.CondQuery) + sets, err := parseNquads(mu.SetNquads) + require.NoError(t, err) + require.EqualValues(t, + &api.NQuad{ + SubjectVar: "v", + Predicate: "name", + ObjectValue: &api.Value{Val: &api.Value_DefaultVal{DefaultVal: "Some One"}}, + }, + sets[0]) + require.EqualValues(t, + &api.NQuad{ + SubjectVar: "v", + Predicate: "email", + ObjectValue: &api.Value{Val: &api.Value_DefaultVal{DefaultVal: "someone@gmail.com"}}, + }, + sets[1]) +} + +func TestParseMutationTxn2(t *testing.T) { + m := ` + txn { + mutation { + set { + uid(v) "Some One" . + uid(v) "someone@gmail.com" . + } + } + }` + mu, err := ParseMutation(m) + require.NoError(t, err) + require.NotNil(t, mu) + sets, err := parseNquads(mu.SetNquads) + require.NoError(t, err) + require.EqualValues(t, + &api.NQuad{ + SubjectVar: "v", + Predicate: "name", + ObjectValue: &api.Value{Val: &api.Value_DefaultVal{DefaultVal: "Some One"}}, + }, + sets[0]) + require.EqualValues(t, + &api.NQuad{ + SubjectVar: "v", + Predicate: "email", + ObjectValue: &api.Value{Val: &api.Value_DefaultVal{DefaultVal: "someone@gmail.com"}}, + }, + sets[1]) +} + +func TestParseMutationTxn3(t *testing.T) { + m := ` + txn { + mutation { + set { + <0x1> "Some One" . + <0x1> "someone@gmail.com" . + <0x1> uid(v) . + } + } + }` + mu, err := ParseMutation(m) + require.NoError(t, err) + require.NotNil(t, mu) + sets, err := parseNquads(mu.SetNquads) + require.NoError(t, err) + require.EqualValues(t, + &api.NQuad{ + Subject: "0x1", + Predicate: "name", + ObjectValue: &api.Value{Val: &api.Value_DefaultVal{DefaultVal: "Some One"}}, + }, + sets[0]) + require.EqualValues(t, + &api.NQuad{ + Subject: "0x1", + Predicate: "email", + ObjectValue: &api.Value{Val: &api.Value_DefaultVal{DefaultVal: "someone@gmail.com"}}, + }, + sets[1]) + require.EqualValues(t, + &api.NQuad{ + Subject: "0x1", + Predicate: "friend", + ObjectVar: "v", + }, + sets[2]) +} + +func TestParseMutationTxn4(t *testing.T) { + m := ` + txn { + mutation { + set { + uid(v) "Some One" . + uid(v) uid(w) . + } + } + }` + mu, err := ParseMutation(m) + require.NoError(t, err) + require.NotNil(t, mu) + sets, err := parseNquads(mu.SetNquads) + require.NoError(t, err) + require.EqualValues(t, + &api.NQuad{ + SubjectVar: "v", + Predicate: "name", + ObjectValue: &api.Value{Val: &api.Value_DefaultVal{DefaultVal: "Some One"}}, + }, + sets[0]) + require.EqualValues(t, + &api.NQuad{ + SubjectVar: "v", + Predicate: "friend", + ObjectVar: "w", + }, + sets[1]) +} + +func TestParseMutationErr1(t *testing.T) { + m := ` + txn { + { + set { + <_:taco> "Some One" . + <_:taco> "someone@gmail.com" . + } + } + }` + _, err := ParseMutation(m) + require.Error(t, err) + require.Contains(t, err.Error(), `Invalid operation type: set`) +} + +func TestParseMutationErr2(t *testing.T) { + m := ` + txn { + query {} + }` + _, err := ParseMutation(m) + require.Error(t, err) + require.Contains(t, err.Error(), `Unexpected "}" inside of txn block.`) +} + +func TestParseMutationErr3(t *testing.T) { + m := ` + txn { + query {} + query {} + }` + _, err := ParseMutation(m) + require.Error(t, err) + require.Contains(t, err.Error(), `Too many query blocks in txn`) +} + +func TestParseMutationErr4(t *testing.T) { + m := ` + txn { + query {} + mutation { + set { + . + . + } + delete { + . + } + ` + _, err := ParseMutation(m) + require.Error(t, err) + require.Contains(t, err.Error(), `Invalid mutation.`) +} + +func TestParseMutationErr5(t *testing.T) { + m := ` + txn { + query { + mutation { + set { + . + . + } + delete { + . + } + ` + _, err := ParseMutation(m) + require.Error(t, err) + require.Contains(t, err.Error(), `Unbalanced '}' found inside query text`) +} + +func TestParseMutationErr6(t *testing.T) { + m := ` + txn{ + query {} + mutation { + set { + . + . + } + delete { + . + } + } + } +` + _, err := ParseMutation(m) + require.Error(t, err) + require.Contains(t, err.Error(), `Unbalanced '}' found inside query text`) +} diff --git a/gql/state.go b/gql/state.go index 7b47d6ec970..553f0fb4d99 100644 --- a/gql/state.go +++ b/gql/state.go @@ -17,7 +17,9 @@ // Package gql is responsible for lexing and parsing a GraphQL query/mutation. package gql -import "github.com/dgraph-io/dgraph/lex" +import ( + "github.com/dgraph-io/dgraph/lex" +) const ( leftCurl = '{' @@ -43,29 +45,155 @@ const ( // Constants representing type of different graphql lexed items. const ( - itemText lex.ItemType = 5 + iota // plain text - itemLeftCurl // left curly bracket - itemRightCurl // right curly bracket - itemEqual // equals to symbol - itemName // [9] names - itemOpType // operation type - itemString // quoted string - itemLeftRound // left round bracket - itemRightRound // right round bracket - itemColon // Colon - itemAt // @ - itemPeriod // . - itemDollar // $ - itemRegex // / - itemBackslash // \ - itemMutationOp // mutation operation - itemMutationContent // mutation content + itemText lex.ItemType = 5 + iota // plain text + itemLeftCurl // left curly bracket + itemRightCurl // right curly bracket + itemEqual // equals to symbol + itemName // [9] names + itemOpType // operation type + itemString // quoted string + itemLeftRound // left round bracket + itemRightRound // right round bracket + itemColon // Colon + itemAt // @ + itemPeriod // . + itemDollar // $ + itemRegex // / + itemBackslash // \ + itemMutationOp // mutation operation + itemMutationOpContent // mutation operation content (inc. query) + itemMutationUpsert // mutation upsert (upsert) + itemMutationUpsertOp // mutation upsert operation (query, mutate) itemLeftSquare itemRightSquare itemComma itemMathOp ) +// lexIdentifyMutationBlock identifies whether it is a mutation upsert block +// If the block begins with "{" => mutation block +// Else if the block begins with "upsert" => mutation upsert block +func lexIdentifyMutationBlock(l *lex.Lexer) lex.StateFn { + l.Mode = lexIdentifyMutationBlock + for { + switch r := l.Next(); { + case isSpace(r) || lex.IsEndOfLine(r): + l.Ignore() + case isNameBegin(r): + l.Backup() + return lexNameMutationBlock + case r == leftCurl: + l.Backup() + return lexInsideMutation + case r == '#': + return lexComment + case r == lex.EOF: + return l.Errorf("Invalid mutation block") + default: + return l.Errorf("Unexpected character while identifying mutation block: %#U", r) + } + } +} + +// lexNameMutationBlock lexes the mutation upsert block +func lexNameMutationBlock(l *lex.Lexer) lex.StateFn { + for { + // The caller already checked isNameBegin, and absorbed one rune. + r := l.Next() + if isNameSuffix(r) { + continue + } + l.Backup() + switch word := l.Input[l.Start:l.Pos]; word { + case "upsert": + l.Emit(itemMutationUpsert) + return lexMutationUpsert + default: + return l.Errorf("Invalid operation type: %s", word) + } + } +} + +// lexMutationUpsert lexes the mutation upsert block +func lexMutationUpsert(l *lex.Lexer) lex.StateFn { + l.Mode = lexMutationUpsert + for { + switch r := l.Next(); { + case r == rightCurl: + l.Depth-- + l.Emit(itemRightCurl) + case r == leftCurl: + l.Depth++ + l.Emit(itemLeftCurl) + case isSpace(r) || lex.IsEndOfLine(r): + l.Ignore() + case isNameBegin(r): + return lexInsideMutationUpsert + case r == '#': + return lexComment + case r == lex.EOF: + return l.Errorf("Unclosed txn action") + default: + return l.Errorf("Unrecognized character in lexInsideTxn: %#U", r) + } + } +} + +// lexInsideMutationUpsert parses the blocks inside mutation upsert block +func lexInsideMutationUpsert(l *lex.Lexer) lex.StateFn { + for { + r := l.Next() + if isNameSuffix(r) { + continue + } + l.Backup() + word := l.Input[l.Start:l.Pos] + switch word { + case "query": + l.Emit(itemMutationUpsertOp) + return lexMutationUpsertQuery + case "mutation": + l.Depth = 0 + l.Emit(itemMutationUpsertOp) + return lexInsideMutation + default: + return l.Errorf("Invalid operation type: %s", word) + } + } +} + +// lexMutationUpsertQuery parses the query block for mutation upsert block +func lexMutationUpsertQuery(l *lex.Lexer) lex.StateFn { + depth := 0 + for { + r := l.Next() + if r == lex.EOF { + return l.Errorf("Unclosed query text") + } + if isSpace(r) || lex.IsEndOfLine(r) { + if depth == 0 { + l.Ignore() + } + continue + } + if r == leftCurl { + depth++ + } + if r == rightCurl { + depth-- + if l.Depth < depth { + return l.Errorf("Unbalanced '}' found inside query text") + } + } + if depth > 0 { + continue + } + l.Emit(itemMutationOpContent) + break + } + return lexInsideMutationUpsert +} + func lexInsideMutation(l *lex.Lexer) lex.StateFn { l.Mode = lexInsideMutation for { @@ -401,7 +529,7 @@ func lexTextMutation(l *lex.Lexer) lex.StateFn { continue } l.Backup() - l.Emit(itemMutationContent) + l.Emit(itemMutationOpContent) break } return lexInsideMutation