diff --git a/gql/parse_txn_block_test.go b/gql/parse_txn_block_test.go new file mode 100644 index 00000000000..3db8ae212f0 --- /dev/null +++ b/gql/parse_txn_block_test.go @@ -0,0 +1,78 @@ +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 := ` + txn { + 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") +} + +// Is this okay? +// - Doesn't contain mutation op inside txn block +// - uid and age are in the same line +func TestMutationTxnBlock3(t *testing.T) { + query := `txn{query{me(func:eq(age,34)){uidfriend{uid age}}}}}` + _, err := ParseMutation(query) + require.Error(t, err) + require.Contains(t, err.Error(), "Too many right curl") +} + +func TestMutationTxnBlock4(t *testing.T) { + query := ` + txn { + mutation { + set { + "_:user1" "45" . + } + } + + query { + me(func: eq(age, 34)) { + uid + friend { + uid + age + } + } + } + } +` + _, err := ParseMutation(query) + require.Nil(t, err) +} diff --git a/gql/parser.go b/gql/parser.go index e5d28c18cbc..205b6d683a5 100644 --- a/gql/parser.go +++ b/gql/parser.go @@ -457,11 +457,9 @@ type Result struct { Schema *pb.SchemaRequest } -// Parse initializes and runs the lexer. It also constructs the GraphQuery subgraph -// from the lexed items. +// Parse initializes and runs the lexer and parser. func Parse(r Request) (res Result, rerr error) { query := r.Str - vmap := convertToVarMap(r.Variables) lexer := lex.NewLexer(query) lexer.Run(lexTopLevel) @@ -469,8 +467,15 @@ func Parse(r Request) (res Result, rerr error) { return res, err } + return ParseQuery(lexer.NewIterator(), r.Variables) +} + +// ParseQuery parses the given query. +// It also constructs the GraphQuery subgraph from the lexed items. +func ParseQuery(it *lex.ItemIterator, vars map[string]string) (res Result, rerr error) { + vmap := convertToVarMap(vars) + var qu *GraphQuery - it := lexer.NewIterator() fmap := make(fragmentMap) for it.Next() { item := it.Item() diff --git a/gql/parser_mutation.go b/gql/parser_mutation.go index 50366e1f0d0..567b4cfd4cb 100644 --- a/gql/parser_mutation.go +++ b/gql/parser_mutation.go @@ -17,22 +17,91 @@ package gql import ( - "errors" + "fmt" "github.com/dgraph-io/dgo/protos/api" "github.com/dgraph-io/dgraph/lex" "github.com/dgraph-io/dgraph/x" ) +// ParseMutation parses a block into a mutation. Returns an object with a mutation or +// a txn block with mutation, otherwise returns nil with an error. func ParseMutation(mutation string) (*api.Mutation, error) { lexer := lex.NewLexer(mutation) - lexer.Run(lexInsideMutation) - it := lexer.NewIterator() - var mu api.Mutation + lexer.Run(lexIdentifyBlock) + if err := lexer.ValidateResult(); err != nil { + return nil, err + } + it := lexer.NewIterator() if !it.Next() { - return nil, errors.New("Invalid mutation") + return nil, x.Errorf("Invalid mutation") + } + + item := it.Item() + switch item.Typ { + case itemMutationTxnBlock: + return ParseTxnBlock(it) + case itemLeftCurl: + return ParseMutationBlock(it) + default: + return nil, x.Errorf("Unexpected token: [%s]", item.Val) } +} + +// ParseTxnBlock parses the txn block +func ParseTxnBlock(it *lex.ItemIterator) (*api.Mutation, error) { + var mu *api.Mutation + var res Result + + // ==>txn<=== {...} + item := it.Item() + if item.Typ != itemMutationTxnBlock { + return nil, x.Errorf("Expected txn block. Got [%s]", item.Val) + } + + // txn ===>{<=== ....} + it.Next() + 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 { + // txn {... ===>}<=== + case itemRightCurl: + // TODO + fmt.Printf("ready to return, but don't know what to do with res: %+v\n", res) + return mu, nil + // txn { ===>mutation<=== {...} query{...}} + // txn { mutation{...} ===>query<==={...}} + case itemMutationTxnBlockOp: + var err error + if item.Val == "query" { + if res, err = ParseQuery(it, nil); err != nil { + return nil, err + } + } else if item.Val == "mutation" { + if mu, err = ParseMutationBlock(it); err != nil { + return nil, err + } + } else { + return nil, x.Errorf("should not reach here") + } + default: + return nil, x.Errorf("unexpected token in txn block [%s]", item.Val) + } + } + + return nil, x.Errorf("Invalid txn 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) @@ -73,7 +142,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..b23e87deff7 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) { diff --git a/gql/state.go b/gql/state.go index 7b47d6ec970..9729381b9b3 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,125 @@ 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) + itemMutationTxnBlock // mutation txn block (txn) + itemMutationTxnBlockOp // mutation txn block operations (query, mutate) itemLeftSquare itemRightSquare itemComma itemMathOp ) +// lexIdentifyBlock identifies whether it is a mutation txn block +// If the block begins with "{" => mutation block +// Else if the block begins with "txn" => mutation txn 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 mutation txn 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 "txn": + l.Emit(itemMutationTxnBlock) + return lexMutationTxnBlock + default: + return l.Errorf("Invalid block: [%s]", word) + } + } +} + +// lexMutationTxnBlock lexes the mutation txn block +func lexMutationTxnBlock(l *lex.Lexer) lex.StateFn { + l.Mode = lexMutationTxnBlock + 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 lexNameTxnOp + case r == '#': + return lexComment + case r == lex.EOF: + return l.Errorf("Unclosed mutation txn action") + default: + return l.Errorf("Unrecognized character in lexMutationTxnBlock: %#U", r) + } + } +} + +// lexNameTxnOp parses the operation names inside mutation txn block +func lexNameTxnOp(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(itemMutationTxnBlockOp) + return lexQuery + case "mutation": + l.Emit(itemMutationTxnBlockOp) + return lexInsideMutation + default: + return l.Errorf("Invalid operation type: %s", word) + } + } +} + func lexInsideMutation(l *lex.Lexer) lex.StateFn { l.Mode = lexInsideMutation for { @@ -231,6 +329,11 @@ func lexFuncOrArg(l *lex.Lexer) lex.StateFn { } func lexTopLevel(l *lex.Lexer) lex.StateFn { + // TODO: we currently only have txn block, this won't work if we have other blocks too. + if l.BlockDepth != 0 { + return lexMutationTxnBlock + } + l.Mode = lexTopLevel Loop: for { @@ -401,7 +504,7 @@ func lexTextMutation(l *lex.Lexer) lex.StateFn { continue } l.Backup() - l.Emit(itemMutationContent) + l.Emit(itemMutationOpContent) break } return lexInsideMutation diff --git a/lex/lexer.go b/lex/lexer.go index 55930320101..cb926331623 100644 --- a/lex/lexer.go +++ b/lex/lexer.go @@ -165,6 +165,7 @@ type Lexer struct { Width int // Width of last rune read from input. widthStack []*RuneWidth items []Item // channel of scanned items. + BlockDepth int // nesting of blocks (e.g. mutation block inside txn block) Depth int // nesting of {} ArgDepth int // nesting of () Mode StateFn // Default state to go back to after reading a token. @@ -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