diff --git a/gql/parser_mutation.go b/gql/parser_mutation.go index 50366e1f0d0..6c8e0d5152c 100644 --- a/gql/parser_mutation.go +++ b/gql/parser_mutation.go @@ -17,22 +17,115 @@ 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) { +// ParseMutation parses a block into a mutation. Returns an object with a mutation or +// an upsert block with mutation, otherwise returns nil with an error. +func ParseMutation(mutation string) (mu *api.Mutation, err error) { lexer := lex.NewLexer(mutation) - lexer.Run(lexInsideMutation) + lexer.Run(lexIdentifyBlock) + if err := lexer.ValidateResult(); err != nil { + return nil, err + } + it := lexer.NewIterator() - var mu api.Mutation + if !it.Next() { + return nil, x.Errorf("Invalid mutation") + } + item := it.Item() + switch item.Typ { + case itemUpsertBlock: + if mu, err = ParseUpsertBlock(it); err != nil { + return nil, err + } + case itemLeftCurl: + if mu, err = ParseMutationBlock(it); err != nil { + return nil, err + } + default: + return nil, x.Errorf("Unexpected token: [%s]", item.Val) + } + + // mutations must be enclosed in a single block. + if 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 +} + +// ParseUpsertBlock parses the upsert block +func ParseUpsertBlock(it *lex.ItemIterator) (*api.Mutation, error) { + var mu *api.Mutation + var queryText string + var queryFound bool + + // ==>upsert<=== {...} if !it.Next() { - return nil, errors.New("Invalid mutation") + return nil, x.Errorf("Unexpected end of upsert block") + } + + // upsert ===>{<=== ....} + item := it.Item() + if item.Typ != itemLeftCurl { + return nil, x.Errorf("Expected { at the start of block. Got: [%s]", item.Val) } + + for it.Next() { + item = it.Item() + switch item.Typ { + // upsert {... ===>}<=== + case itemRightCurl: + if mu == nil { + return nil, x.Errorf("empty mutation block") + } else if !queryFound { + return nil, x.Errorf("query op not found in upsert block") + } else { + mu.Query = queryText + return mu, nil + } + + // upsert { ===>mutation<=== {...} query{...}} + // upsert { mutation{...} ===>query<==={...}} + case itemUpsertBlockOp: + if !it.Next() { + return nil, x.Errorf("Unexpected end of upsert block") + } + if item.Val == "query" { + if queryFound { + return nil, x.Errorf("multiple query ops inside upsert block") + } + queryFound = true + x.AssertTrue(it.Item().Typ == itemUpsertBlockOpContent) + queryText += it.Item().Val + } else if item.Val == "mutation" { + var err error + if mu, err = ParseMutationBlock(it); err != nil { + return nil, err + } + } else if item.Val == "fragment" { + x.AssertTrue(it.Item().Typ == itemUpsertBlockOpContent) + queryText += "fragment" + it.Item().Val + } else { + return nil, x.Errorf("should not reach here") + } + + default: + return nil, x.Errorf("unexpected token in upsert block [%s]", item.Val) + } + } + + return nil, x.Errorf("Invalid upsert block") +} + +// ParseMutationBlock parses the mutation block +func ParseMutationBlock(it *lex.ItemIterator) (*api.Mutation, error) { + var mu api.Mutation + item := it.Item() if item.Typ != itemLeftCurl { return nil, x.Errorf("Expected { at the start of block. Got: [%s]", item.Val) @@ -44,10 +137,6 @@ func ParseMutation(mutation string) (*api.Mutation, error) { continue } if item.Typ == itemRightCurl { - // mutations must be enclosed in a single block. - if 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 { @@ -73,7 +162,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_test.go b/gql/parser_test.go index 6d674bce683..0cd060aa068 100644 --- a/gql/parser_test.go +++ b/gql/parser_test.go @@ -1641,7 +1641,7 @@ func TestParseMutationError(t *testing.T) { ` _, err := ParseMutation(query) require.Error(t, err) - require.Equal(t, `Expected { at the start of block. Got: [mutation]`, err.Error()) + require.Contains(t, err.Error(), `Invalid block: [mutation]`) } func TestParseMutationError2(t *testing.T) { @@ -1656,7 +1656,7 @@ func TestParseMutationError2(t *testing.T) { ` _, err := ParseMutation(query) require.Error(t, err) - require.Equal(t, `Expected { at the start of block. Got: [set]`, err.Error()) + require.Contains(t, err.Error(), `Invalid block: [set]`) } func TestParseMutationAndQueryWithComments(t *testing.T) { @@ -4362,10 +4362,10 @@ func TestParseMutationTooManyBlocks(t *testing.T) { }{ set { _:b2 "b2 content" . } }`, - errStr: "Unexpected { after the end of the block.", + errStr: "Unrecognized character in lexText", }, {m: `{set { _:a1 "a1 content" . }} something`, - errStr: "Invalid operation type: something after the end of the block", + errStr: "Invalid operation type: something", }, {m: ` # comments are ok diff --git a/gql/state.go b/gql/state.go index 7b47d6ec970..7f72e22b797 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,154 @@ 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 (set, delete) + itemMutationOpContent // mutation operation content (inc. query) + itemUpsertBlock // mutation upsert block + itemUpsertBlockOp // mutation upsert block operations (query, mutate, fragment) + itemUpsertBlockOpContent // mutation upsert block operations' content itemLeftSquare itemRightSquare itemComma itemMathOp ) +// lexIdentifyBlock identifies whether it is an upsert block +// If the block begins with "{" => mutation block +// Else if the block begins with "upsert" => upsert block +func lexIdentifyBlock(l *lex.Lexer) lex.StateFn { + l.Mode = lexIdentifyBlock + for { + switch r := l.Next(); { + case isSpace(r) || lex.IsEndOfLine(r): + l.Ignore() + case isNameBegin(r): + return lexNameBlock + 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) + } + } +} + +// lexNameBlock lexes the blocks, for now, only upsert block +func lexNameBlock(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(itemUpsertBlock) + return lexUpsertBlock + default: + return l.Errorf("Invalid block: [%s]", word) + } + } +} + +// lexUpsertBlock lexes the upsert block +func lexUpsertBlock(l *lex.Lexer) lex.StateFn { + l.Mode = lexUpsertBlock + for { + switch r := l.Next(); { + case r == rightCurl: + l.BlockDepth-- + l.Emit(itemRightCurl) + if l.BlockDepth == 0 { + return lexTopLevel + } + case r == leftCurl: + l.BlockDepth++ + l.Emit(itemLeftCurl) + case isSpace(r) || lex.IsEndOfLine(r): + l.Ignore() + case isNameBegin(r): + return lexNameUpsertOp + case r == '#': + return lexComment + case r == lex.EOF: + return l.Errorf("Unclosed upsert block") + default: + return l.Errorf("Unrecognized character in upsert block: %#U", r) + } + } +} + +// lexNameUpsertOp parses the operation names inside upsert block +func lexNameUpsertOp(l *lex.Lexer) lex.StateFn { + for { + // The caller already checked isNameBegin, and absorbed one rune. + r := l.Next() + if isNameSuffix(r) { + continue + } + l.Backup() + word := l.Input[l.Start:l.Pos] + switch word { + case "query": + l.Emit(itemUpsertBlockOp) + return lexBlockContent + case "mutation": + l.Emit(itemUpsertBlockOp) + return lexInsideMutation + case "fragment": + l.Emit(itemUpsertBlockOp) + return lexBlockContent + default: + return l.Errorf("Invalid operation type: %s", word) + } + } +} + +// lexBlockContent lexes and absorbs the text inside a block (covered by braces). +func lexBlockContent(l *lex.Lexer) lex.StateFn { + depth := 0 + for { + switch l.Next() { + case lex.EOF: + return l.Errorf("Unclosed block (matching braces not found)") + case quote: + if err := l.LexQuotedString(); err != nil { + return l.Errorf(err.Error()) + } + case leftCurl: + depth++ + case rightCurl: + depth-- + if depth < 0 { + return l.Errorf("Unopened } found") + } else if depth == 0 { + l.Emit(itemUpsertBlockOpContent) + return lexUpsertBlock + } + } + } +} + func lexInsideMutation(l *lex.Lexer) lex.StateFn { l.Mode = lexInsideMutation for { @@ -231,6 +358,12 @@ func lexFuncOrArg(l *lex.Lexer) lex.StateFn { } func lexTopLevel(l *lex.Lexer) lex.StateFn { + // TODO (Aman): we currently only have upsert block, + // this won't work if we have other blocks too. + if l.BlockDepth != 0 { + return lexUpsertBlock + } + l.Mode = lexTopLevel Loop: for { @@ -401,7 +534,7 @@ func lexTextMutation(l *lex.Lexer) lex.StateFn { continue } l.Backup() - l.Emit(itemMutationContent) + l.Emit(itemMutationOpContent) break } return lexInsideMutation diff --git a/gql/upsert_test.go b/gql/upsert_test.go new file mode 100644 index 00000000000..1defc69c0fb --- /dev/null +++ b/gql/upsert_test.go @@ -0,0 +1,275 @@ +package gql + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMutationTxnBlock1(t *testing.T) { + query := ` + query { + me(func: eq(age, 34)) { + uid + friend { + uid + age + } + } + } +` + _, err := ParseMutation(query) + require.Error(t, err) + require.Contains(t, err.Error(), "Invalid block: [query]") +} + +func TestMutationTxnBlock2(t *testing.T) { + query := ` + upsert { + query { + me(func: eq(age, 34)) { + uid + friend { + uid + age + } + } + } + } +} +` + _, err := ParseMutation(query) + require.Error(t, err) + require.Contains(t, err.Error(), "Too many right curl") +} + +func TestMutationTxnBlock3(t *testing.T) { + query := ` + upsert { + query { + me(func: eq(age, 34)) { + uid + friend { + uid age + } + } + } + } +` + _, err := ParseMutation(query) + require.Error(t, err) + require.Contains(t, err.Error(), "empty mutation block") +} + +func TestMutationTxnBlock4(t *testing.T) { + query := ` + upsert { + query { + me(func: eq(age, 34)) { + uid + friend { + uid + age + } + } + } + + mutation { + set { + "_:user1" "45" . + } + } + } +` + _, err := ParseMutation(query) + require.Nil(t, err) +} + +func TestMutationTxnBlock5(t *testing.T) { + query := ` + upsert { + mutation { + set { + "_:user1" "45" . + } + } + + query { + me(func: eq(age, 34)) { + uid + friend { + uid + age + } + } + } + } +` + _, err := ParseMutation(query) + require.Nil(t, err) +} + +func TestMutationTxnBlock6(t *testing.T) { + query := ` + upsert { + mutation { + set { + "_:user1" "45" . + } + } + + query { + me(func: eq(age, 34)) { + uid + friend { + uid + age + } + } + } + + query { + me2(func: eq(age, 34)) { + uid + friend { + uid + age + } + } + } + } +` + _, err := ParseMutation(query) + require.Error(t, err) + require.Contains(t, err.Error(), "multiple query ops inside upsert block") +} + +func TestMutationTxnBlock7(t *testing.T) { + query := `upsert {}` + _, err := ParseMutation(query) + require.Error(t, err) + require.Contains(t, err.Error(), "empty mutation block") +} + +func TestMutationTxnBlock8(t *testing.T) { + query := `upsert {` + _, err := ParseMutation(query) + require.Contains(t, err.Error(), "Unclosed upsert block") +} + +func TestMutationTxnBlock9(t *testing.T) { + query := ` + upsert { + mutation { + set { + "_:user1" "45" . + } + } + + query { + me(func: eq(age, 34)) { + ...fragmentA + friend { + ...fragmentA + age + } + } + } + + fragment fragmentA { + uid + } + } +` + _, err := ParseMutation(query) + require.Nil(t, err) +} + +func TestMutationTxnBlock10(t *testing.T) { + query := ` + upsert { + mutation { + set { + "_:user1" "45" . + } + } + + query { + me(func: eq(age, "{")) { + uid + friend { + uid + age + } + } + } + } +` + _, err := ParseMutation(query) + require.Nil(t, err) +} + +func TestMutationTxnBlock11(t *testing.T) { + query := ` + upsert { + mutation { + set { + "_:user1" "45" . + } + } + + query { + me(func: eq(age, "{ +` + _, err := ParseMutation(query) + require.Error(t, err) + require.Contains(t, err.Error(), "Unexpected end of input") +} + +func TestMutationTxnBlock12(t *testing.T) { + query := ` + upsert + + { + mutation + + { + set + { + "_:user1" "45" . + + # This is a comment + "_:user1" "{vishesh" . + }} + + query + + { + me(func: eq(age, "{")) { + uid + friend { + uid + age + } + } + } + } +` + _, err := ParseMutation(query) + require.Nil(t, err) +} + +func TestMutationTxnBlock13(t *testing.T) { + query := ` + upsert { + mutation { + set { + "_:user1" "45" . + } + } + } +` + _, err := ParseMutation(query) + require.Error(t, err) + require.Contains(t, err.Error(), "query op not found in upsert block") +} diff --git a/lex/lexer.go b/lex/lexer.go index 55930320101..b8685e68752 100644 --- a/lex/lexer.go +++ b/lex/lexer.go @@ -166,6 +166,7 @@ type Lexer struct { widthStack []*RuneWidth items []Item // channel of scanned items. Depth int // nesting of {} + BlockDepth int // nesting of blocks (e.g. mutation block inside upsert block) ArgDepth int // nesting of () Mode StateFn // Default state to go back to after reading a token. Line int // the current line number corresponding to Start @@ -194,7 +195,7 @@ func (l *Lexer) ValidateResult() error { func (l *Lexer) Run(f StateFn) *Lexer { for state := f; state != nil; { // The following statement is useful for debugging. - //fmt.Printf("Func: %v\n", runtime.FuncForPC(reflect.ValueOf(state).Pointer()).Name()) + // fmt.Printf("Func: %v\n", runtime.FuncForPC(reflect.ValueOf(state).Pointer()).Name()) state = state(l) } return l