diff --git a/gql/parser.go b/gql/parser.go index 39f5ddc7c29d4f27368b07ea349a7c21e7411d73..7892595072a48b3b1220892a9618df0d4f24a917 100644 --- a/gql/parser.go +++ b/gql/parser.go @@ -36,6 +36,11 @@ type GraphQuery struct { Children []*GraphQuery } +type Mutation struct { + Set string + Del string +} + func run(l *lex.Lexer) { for state := lexText; state != nil; { state = state(l) @@ -43,10 +48,11 @@ func run(l *lex.Lexer) { close(l.Items) // No more tokens. } -func Parse(input string) (gq *GraphQuery, rerr error) { +func Parse(input string) (gq *GraphQuery, mu *Mutation, rerr error) { l := lex.NewLexer(input) go run(l) + mu = nil gq = nil for item := range l.Items { if item.Typ == itemText { @@ -54,7 +60,13 @@ func Parse(input string) (gq *GraphQuery, rerr error) { } if item.Typ == itemOpType { if item.Val == "mutation" { - return nil, errors.New("Mutations not supported") + if mu != nil { + return nil, nil, errors.New("Only one mutation block allowed.") + } + mu, rerr = getMutation(l) + if rerr != nil { + return nil, nil, rerr + } } } if item.Typ == itemLeftCurl { @@ -62,16 +74,71 @@ func Parse(input string) (gq *GraphQuery, rerr error) { gq, rerr = getRoot(l) if rerr != nil { x.Err(glog, rerr).Error("While retrieving subgraph root") - return nil, rerr + return nil, nil, rerr } } else { if err := godeep(l, gq); err != nil { - return gq, err + return nil, nil, err } } } } - return gq, nil + return gq, mu, nil +} + +func getMutation(l *lex.Lexer) (mu *Mutation, rerr error) { + for item := range l.Items { + if item.Typ == itemText { + continue + } + if item.Typ == itemLeftCurl { + mu = new(Mutation) + } + if item.Typ == itemRightCurl { + return mu, nil + } + if item.Typ == itemMutationOp { + if err := parseMutationOp(l, item.Val, mu); err != nil { + return nil, err + } + } + } + return nil, errors.New("Invalid mutation.") +} + +func parseMutationOp(l *lex.Lexer, op string, mu *Mutation) error { + if mu == nil { + return errors.New("Mutation is nil.") + } + + parse := false + for item := range l.Items { + if item.Typ == itemText { + continue + } + if item.Typ == itemLeftCurl { + if parse { + return errors.New("Too many left curls in set mutation.") + } + parse = true + } + if item.Typ == itemMutationContent { + if !parse { + return errors.New("Mutation syntax invalid.") + } + if op == "set" { + mu.Set = item.Val + } else if op == "delete" { + mu.Del = item.Val + } else { + return errors.New("Invalid mutation operation.") + } + } + if item.Typ == itemRightCurl { + return nil + } + } + return errors.New("Invalid mutation formatting.") } func getRoot(l *lex.Lexer) (gq *GraphQuery, rerr error) { diff --git a/gql/parser_test.go b/gql/parser_test.go index d8be58c5c029197a6605607154efd9194ee3126d..c1c00ddaa60fe574e6f70608c19e10ca7825dbfb 100644 --- a/gql/parser_test.go +++ b/gql/parser_test.go @@ -18,6 +18,7 @@ package gql import ( "fmt" + "strings" "testing" ) @@ -41,7 +42,7 @@ func TestParse(t *testing.T) { } ` - gq, err := Parse(query) + gq, _, err := Parse(query) if err != nil { t.Error(err) } @@ -82,7 +83,7 @@ func TestParseXid(t *testing.T) { type.object.name } }` - gq, err := Parse(query) + gq, _, err := Parse(query) if err != nil { t.Error(err) return @@ -99,22 +100,6 @@ func TestParseXid(t *testing.T) { } } -func TestParse_error1(t *testing.T) { - query := ` - mutation { - me(_uid_:0x0a) { - name - } - } - ` - var err error - _, err = Parse(query) - t.Log(err) - if err == nil { - t.Error("Expected error") - } -} - func TestParse_error2(t *testing.T) { query := ` query { @@ -124,7 +109,7 @@ func TestParse_error2(t *testing.T) { } ` var err error - _, err = Parse(query) + _, _, err = Parse(query) t.Log(err) if err == nil { t.Error("Expected error") @@ -141,7 +126,7 @@ func TestParse_pass1(t *testing.T) { } } ` - gq, err := Parse(query) + gq, _, err := Parse(query) if err != nil { t.Error(err) } @@ -168,7 +153,7 @@ func TestParse_block(t *testing.T) { } } ` - gq, err := Parse(query) + gq, _, err := Parse(query) if err != nil { t.Error(err) } @@ -179,3 +164,134 @@ func TestParse_block(t *testing.T) { t.Error(err) } } + +func TestParseMutation(t *testing.T) { + query := ` + mutation { + set { + <name> <is> <something> . + <hometown> <is> <san francisco> . + } + delete { + <name> <is> <something-else> . + } + } + ` + _, mu, err := Parse(query) + if err != nil { + t.Error(err) + return + } + if strings.Index(mu.Set, "<name> <is> <something> .") == -1 { + t.Error("Unable to find mutation content.") + } + if strings.Index(mu.Set, "<hometown> <is> <san francisco> .") == -1 { + t.Error("Unable to find mutation content.") + } + if strings.Index(mu.Del, "<name> <is> <something-else> .") == -1 { + t.Error("Unable to find mutation content.") + } +} + +func TestParseMutation_error(t *testing.T) { + query := ` + mutation { + set { + <name> <is> <something> . + <hometown> <is> <san francisco> . + } + delete { + <name> <is> <something-else> . + } + ` + _, _, err := Parse(query) + if err == nil { + t.Error(err) + return + } + t.Log(err) +} + +func TestParseMutation_error2(t *testing.T) { + query := ` + mutation { + set { + <name> <is> <something> . + <hometown> <is> <san francisco> . + } + delete { + <name> <is> <something-else> . + } + } + mutation { + set { + another one? + } + } + + ` + _, _, err := Parse(query) + if err == nil { + t.Error(err) + return + } + t.Log(err) +} + +func TestParseMutationAndQuery(t *testing.T) { + query := ` + mutation { + set { + <name> <is> <something> . + <hometown> <is> <san francisco> . + } + delete { + <name> <is> <something-else> . + } + } + query { + me(_xid_: tomhanks) { + name + hometown + } + } + ` + gq, mu, err := Parse(query) + if err != nil { + t.Error(err) + return + } + + if mu == nil { + t.Error("mutation is nil") + return + } + if strings.Index(mu.Set, "<name> <is> <something> .") == -1 { + t.Error("Unable to find mutation content.") + } + if strings.Index(mu.Set, "<hometown> <is> <san francisco> .") == -1 { + t.Error("Unable to find mutation content.") + } + if strings.Index(mu.Del, "<name> <is> <something-else> .") == -1 { + t.Error("Unable to find mutation content.") + } + + if gq == nil { + t.Error("subgraph is nil") + return + } + if gq.XID != "tomhanks" { + t.Error("Expected: tomhanks. Got: %v", gq.XID) + return + } + if len(gq.Children) != 2 { + t.Errorf("Expected 2 children. Got: %v", len(gq.Children)) + return + } + if err := checkAttr(gq.Children[0], "name"); err != nil { + t.Error(err) + } + if err := checkAttr(gq.Children[1], "hometown"); err != nil { + t.Error(err) + } +} diff --git a/gql/state.go b/gql/state.go index 9313671b021fc2f8ff01e63c7732f232a80c6226..2370108d8f51fd21e3b8f9654f3b4756d0750b22 100644 --- a/gql/state.go +++ b/gql/state.go @@ -19,22 +19,26 @@ package gql import "github.com/dgraph-io/dgraph/lex" const ( - leftCurl = '{' - rightCurl = '}' + leftCurl = '{' + rightCurl = '}' + queryMode = 1 + mutationMode = 2 ) const ( - itemText lex.ItemType = 5 + iota // plain text - itemLeftCurl // left curly bracket - itemRightCurl // right curly bracket - itemComment // comment - itemName // [9] names - itemOpType // operation type - itemString // quoted string - itemLeftRound // left round bracket - itemRightRound // right round bracket - itemArgName // argument name - itemArgVal // argument val + itemText lex.ItemType = 5 + iota // plain text + itemLeftCurl // left curly bracket + itemRightCurl // right curly bracket + itemComment // comment + itemName // [9] names + itemOpType // operation type + itemString // quoted string + itemLeftRound // left round bracket + itemRightRound // right round bracket + itemArgName // argument name + itemArgVal // argument val + itemMutationOp // mutation operation + itemMutationContent // mutation content ) func lexText(l *lex.Lexer) lex.StateFn { @@ -47,7 +51,11 @@ Loop: l.Next() // advance one to get back to where we saw leftCurl. l.Depth += 1 // one level down. l.Emit(itemLeftCurl) - return lexInside // we're in. + if l.Mode == mutationMode { + return lexInsideMutation + } else { + return lexInside + } case r == rightCurl: return l.Errorf("Too many right characters") @@ -79,7 +87,7 @@ func lexInside(l *lex.Lexer) lex.StateFn { l.Depth += 1 l.Emit(itemLeftCurl) case r == lex.EOF: - return l.Errorf("unclosed action") + return l.Errorf("Unclosed action") case isSpace(r) || isEndOfLine(r) || r == ',': l.Ignore() case isNameBegin(r): @@ -128,6 +136,64 @@ func lexComment(l *lex.Lexer) lex.StateFn { return nil // Stop the run loop. } +func lexInsideMutation(l *lex.Lexer) lex.StateFn { + for { + switch r := l.Next(); { + case r == rightCurl: + l.Depth -= 1 + l.Emit(itemRightCurl) + if l.Depth == 0 { + return lexText + } + case r == leftCurl: + l.Depth += 1 + l.Emit(itemLeftCurl) + if l.Depth >= 2 { + return lexTextMutation + } + case r == lex.EOF: + return l.Errorf("Unclosed mutation action") + case isSpace(r) || isEndOfLine(r): + l.Ignore() + case isNameBegin(r): + return lexNameMutation + default: + return l.Errorf("Unrecognized character in lexInsideMutation: %#U", r) + } + } +} + +func lexNameMutation(l *lex.Lexer) lex.StateFn { + for { + // The caller already checked isNameBegin, and absorbed one rune. + r := l.Next() + if isNameBegin(r) { + continue + } + l.Backup() + l.Emit(itemMutationOp) + break + } + return lexInsideMutation +} + +func lexTextMutation(l *lex.Lexer) lex.StateFn { + for { + r := l.Next() + if r == lex.EOF { + return l.Errorf("Unclosed mutation text") + } + if r != rightCurl { + // Absorb everything until we find '}'. + continue + } + l.Backup() + l.Emit(itemMutationContent) + break + } + return lexInsideMutation +} + func lexOperationType(l *lex.Lexer) lex.StateFn { for { r := l.Next() @@ -136,8 +202,13 @@ func lexOperationType(l *lex.Lexer) lex.StateFn { } l.Backup() word := l.Input[l.Start:l.Pos] - if word == "query" || word == "mutation" { + if word == "mutation" { + l.Emit(itemOpType) + l.Mode = mutationMode + + } else if word == "query" { l.Emit(itemOpType) + l.Mode = queryMode } break } diff --git a/gql/state_test.go b/gql/state_test.go index a1ce13d61805ea6f4c56a8acac731792f76c2da8..69ff92730141c52d6282dc956dd4dc359fc9bd39 100644 --- a/gql/state_test.go +++ b/gql/state_test.go @@ -17,7 +17,6 @@ package gql import ( - "fmt" "testing" "github.com/dgraph-io/dgraph/lex" @@ -25,7 +24,7 @@ import ( func TestNewLexer(t *testing.T) { input := ` - mutation { + query { me( id: 10, xid: rick ) { name0 # my name _city, # 0what would fail lex. @@ -38,6 +37,56 @@ func TestNewLexer(t *testing.T) { l := lex.NewLexer(input) go run(l) for item := range l.Items { - fmt.Println(item.String()) + if item.Typ == lex.ItemError { + t.Error(item.String()) + } + t.Log(item.String()) + } +} + +func TestNewLexerMutation(t *testing.T) { + input := ` + mutation { + set { + What is <this> . + Why is this #!!? + How is this? + } + delete { + Why is this + } + } + query { + me(xid: rick) { + _city + } + }` + l := lex.NewLexer(input) + go run(l) + for item := range l.Items { + if item.Typ == lex.ItemError { + t.Error(item.String()) + } + t.Log(item.String()) + } +} + +func TestAbruptMutation(t *testing.T) { + input := ` + mutation { + set { + What is <this> . + Why is this #!!? + How is this? + }` + l := lex.NewLexer(input) + go run(l) + var typ lex.ItemType + for item := range l.Items { + t.Log(item.String()) + typ = item.Typ + } + if typ != lex.ItemError { + t.Error("This should fail.") } } diff --git a/lex/lexer.go b/lex/lexer.go index 2b4b3cabd56b4b5e1f2df588a1f6a0f37da7bd03..9027e11044c0e08449a18179af0902d37abe3702 100644 --- a/lex/lexer.go +++ b/lex/lexer.go @@ -63,6 +63,7 @@ type Lexer struct { Width int // width of last rune read from input. Items chan item // channel of scanned items. Depth int // nesting of {} + Mode int // mode based on information so far. } func NewLexer(input string) *Lexer { diff --git a/query/query_test.go b/query/query_test.go index c11f2b1aadc82909754b429757a1b9fc8856a8a7..44bbbd07c93bf4403f9ee393bdb913b7f6dcf999 100644 --- a/query/query_test.go +++ b/query/query_test.go @@ -190,7 +190,7 @@ func TestProcessGraph(t *testing.T) { } } ` - gq, err := gql.Parse(query) + gq, _, err := gql.Parse(query) if err != nil { t.Error(err) } @@ -282,7 +282,7 @@ func TestToJson(t *testing.T) { } ` - gq, err := gql.Parse(query) + gq, _, err := gql.Parse(query) if err != nil { t.Error(err) } diff --git a/server/main.go b/server/main.go index 5085ff9fdef228fa478b87c3cd792cd117d2008f..11c240aa6ed1d08e2132f0e86ab63faac5e92bd4 100644 --- a/server/main.go +++ b/server/main.go @@ -72,7 +72,7 @@ func queryHandler(w http.ResponseWriter, r *http.Request) { } glog.WithField("q", string(q)).Debug("Query received.") - gq, err := gql.Parse(string(q)) + gq, _, err := gql.Parse(string(q)) if err != nil { x.Err(glog, err).Error("While parsing query") x.SetStatus(w, x.E_INVALID_REQUEST, err.Error()) diff --git a/server/main_test.go b/server/main_test.go index 0c384f160c57d5b934f2ee43921a92b82295f601..010195130e0932fbebc2c57546c2476276643fe9 100644 --- a/server/main_test.go +++ b/server/main_test.go @@ -88,7 +88,7 @@ func TestQuery(t *testing.T) { defer closeAll(dir1, dir2, clog) // Parse GQL into internal query representation. - gq, err := gql.Parse(q0) + gq, _, err := gql.Parse(q0) if err != nil { t.Error(err) return @@ -184,7 +184,7 @@ func BenchmarkQuery(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - gq, err := gql.Parse(q1) + gq, _, err := gql.Parse(q1) if err != nil { b.Error(err) return