Add update mutation

This commit is contained in:
Vikram Rangnekar 2019-09-06 00:34:23 -04:00
parent 9613a0153b
commit e765176bfa
5 changed files with 213 additions and 113 deletions

View File

@ -1,5 +1,68 @@
# http://localhost:8080/ # http://localhost:8080/
variables {
"update": {
"name": "Hellooooo",
"description": "World",
"created_at": "now",
"updated_at": "now"
},
"user": 123
}
mutation {
products(update: $update, where: {id: {eq: 134}}) {
id
name
description
}
}
variables {
"update": {
"name": "Hellooooo",
"description": "World !!!!!"
},
"user": 123
}
mutation {
products(id: 5, update: $update) {
id
name
description
}
}
variables {
"id": 5
}
{
products(id: $ID) {
id
name
description
}
}
variables {
"update": {
"name": "Hellooooo",
"description": "World"
},
"user": 123
}
mutation {
products(update: $update, where: {id: {eq: 134}}) {
id
name
description
}
}
query { query {
me { me {
id id
@ -32,94 +95,3 @@ query {
} }
} }
query {
products(
limit: 30
order_by: { price: desc }
distinct: [price]
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }
) {
id
name
price
user {
id
email
}
}
variables {
"insert": {
"name": "Hello",
"description": "World",
"created_at": "now",
"updated_at": "now"
},
"user": 123
}
mutation {
products(insert: $insert) {
id
name
description
}
}
variables {
"insert": {
"name": "Hello",
"description": "World",
"created_at": "now",
"updated_at": "now"
},
"user": 123
}
mutation {
products(insert: $insert) {
id
}
}
variables {
"insert": {
"description": "World3",
"name": "Hello3",
"created_at": "now",
"updated_at": "now"
},
"user": 123
}
{
customers {
id
email
payments {
customer_id
amount
billing_details
}
}
}
variables {
"insert": {
"description": "World3",
"name": "Hello3",
"created_at": "now",
"updated_at": "now"
},
"user": 123
}
{
me {
id
full_name
}
}

View File

@ -3,6 +3,7 @@ package psql
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"io" "io"
"github.com/dosco/super-graph/jsn" "github.com/dosco/super-graph/jsn"
@ -21,8 +22,19 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w *bytes.Buffer, vars Varia
c.w.WriteString(root.Table) c.w.WriteString(root.Table)
c.w.WriteString(` AS (`) c.w.WriteString(` AS (`)
if _, err := c.renderInsert(qc, w, vars); err != nil { switch root.Action {
return 0, err case qcode.ActionInsert:
if _, err := c.renderInsert(qc, w, vars); err != nil {
return 0, err
}
case qcode.ActionUpdate:
if _, err := c.renderUpdate(qc, w, vars); err != nil {
return 0, err
}
default:
return 0, errors.New("valid mutations are 'insert' and 'update'")
} }
c.w.WriteString(`) `) c.w.WriteString(`) `)
@ -33,9 +45,14 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w *bytes.Buffer, vars Varia
func (c *compilerContext) renderInsert(qc *qcode.QCode, w *bytes.Buffer, vars Variables) (uint32, error) { func (c *compilerContext) renderInsert(qc *qcode.QCode, w *bytes.Buffer, vars Variables) (uint32, error) {
root := &qc.Selects[0] root := &qc.Selects[0]
insert, ok := vars["insert"] insert, ok := vars[root.ActionVar]
if !ok { if !ok {
return 0, errors.New("Variable 'insert' not defined") return 0, fmt.Errorf("Variable '%s' not defined", root.ActionVar)
}
ti, err := c.schema.GetTable(root.Table)
if err != nil {
return 0, err
} }
jt, array, err := jsn.Tree(insert) jt, array, err := jsn.Tree(insert)
@ -45,12 +62,12 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w *bytes.Buffer, vars Va
c.w.WriteString(`WITH input AS (SELECT {{insert}}::json AS j) INSERT INTO `) c.w.WriteString(`WITH input AS (SELECT {{insert}}::json AS j) INSERT INTO `)
c.w.WriteString(root.Table) c.w.WriteString(root.Table)
io.WriteString(c.w, " (") io.WriteString(c.w, ` (`)
c.renderInsertColumns(qc, w, jt) c.renderInsertColumns(qc, w, jt, ti)
io.WriteString(c.w, ")") io.WriteString(c.w, `)`)
c.w.WriteString(` SELECT `) c.w.WriteString(` SELECT `)
c.renderInsertColumns(qc, w, jt) c.renderInsertColumns(qc, w, jt, ti)
c.w.WriteString(` FROM input i, `) c.w.WriteString(` FROM input i, `)
if array { if array {
@ -67,12 +84,7 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w *bytes.Buffer, vars Va
} }
func (c *compilerContext) renderInsertColumns(qc *qcode.QCode, w *bytes.Buffer, func (c *compilerContext) renderInsertColumns(qc *qcode.QCode, w *bytes.Buffer,
jt map[string]interface{}) (uint32, error) { jt map[string]interface{}, ti *DBTableInfo) (uint32, error) {
ti, err := c.schema.GetTable(qc.Selects[0].Table)
if err != nil {
return 0, err
}
i := 0 i := 0
for _, cn := range ti.ColumnNames { for _, cn := range ti.ColumnNames {
@ -80,7 +92,7 @@ func (c *compilerContext) renderInsertColumns(qc *qcode.QCode, w *bytes.Buffer,
continue continue
} }
if i != 0 { if i != 0 {
io.WriteString(c.w, ", ") io.WriteString(c.w, `, `)
} }
c.w.WriteString(cn) c.w.WriteString(cn)
i++ i++
@ -88,3 +100,72 @@ func (c *compilerContext) renderInsertColumns(qc *qcode.QCode, w *bytes.Buffer,
return 0, nil return 0, nil
} }
func (c *compilerContext) renderUpdate(qc *qcode.QCode, w *bytes.Buffer, vars Variables) (uint32, error) {
root := &qc.Selects[0]
update, ok := vars[root.ActionVar]
if !ok {
return 0, fmt.Errorf("Variable '%s' not defined", root.ActionVar)
}
ti, err := c.schema.GetTable(root.Table)
if err != nil {
return 0, err
}
jt, array, err := jsn.Tree(update)
if err != nil {
return 0, err
}
c.w.WriteString(`WITH input AS (SELECT {{update}}::json AS j) UPDATE `)
c.w.WriteString(root.Table)
io.WriteString(c.w, ` SET (`)
c.renderInsertColumns(qc, w, jt, ti)
c.w.WriteString(`) = (SELECT `)
c.renderInsertColumns(qc, w, jt, ti)
c.w.WriteString(` FROM input i, `)
if array {
c.w.WriteString(`json_populate_recordset`)
} else {
c.w.WriteString(`json_populate_record`)
}
c.w.WriteString(`(NULL::`)
c.w.WriteString(root.Table)
c.w.WriteString(`, i.j) t)`)
io.WriteString(c.w, ` WHERE `)
if err := c.renderWhere(root, ti); err != nil {
return 0, err
}
io.WriteString(c.w, ` RETURNING *`)
return 0, nil
}
func (c *compilerContext) renderUpdateColumns(qc *qcode.QCode, w *bytes.Buffer,
jt map[string]interface{}, ti *DBTableInfo) (uint32, error) {
i := 0
for _, cn := range ti.ColumnNames {
if _, ok := jt[cn]; !ok {
continue
}
if i != 0 {
io.WriteString(c.w, `, `)
}
c.w.WriteString(cn)
c.w.WriteString(` = {{`)
c.w.WriteString(cn)
c.w.WriteString(`}}`)
i++
}
return 0, nil
}

View File

@ -14,7 +14,7 @@ func singleInsert(t *testing.T) {
} }
}` }`
sql := `test` sql := `WITH product AS (WITH input AS (SELECT {{insert}}::json AS j) INSERT INTO product (name, description) SELECT name, description FROM input i, json_populate_record(NULL::product, i.j) t RETURNING * ) SELECT json_object_agg('product', product) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "product_0"."id" AS "id", "product_0"."name" AS "name") AS "sel_0")) AS "product" FROM (SELECT "product"."id", "product"."name" FROM "products" AS "product" WHERE ((("product"."price") > 0) AND (("product"."price") < 8) AND (("id") = 15)) LIMIT ('1') :: integer) AS "product_0" LIMIT ('1') :: integer) AS "done_1337";`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"insert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), "insert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`),
@ -25,8 +25,6 @@ func singleInsert(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
fmt.Println(">", string(resSQL))
if string(resSQL) != sql { if string(resSQL) != sql {
t.Fatal(errNotExpected) t.Fatal(errNotExpected)
} }
@ -40,7 +38,7 @@ func bulkInsert(t *testing.T) {
} }
}` }`
sql := `test` sql := `WITH product AS (WITH input AS (SELECT {{insert}}::json AS j) INSERT INTO product (name, description) SELECT name, description FROM input i, json_populate_recordset(NULL::product, i.j) t RETURNING * ) SELECT json_object_agg('product', product) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "product_0"."id" AS "id", "product_0"."name" AS "name") AS "sel_0")) AS "product" FROM (SELECT "product"."id", "product"."name" FROM "products" AS "product" WHERE ((("product"."price") > 0) AND (("product"."price") < 8) AND (("id") = 15)) LIMIT ('1') :: integer) AS "product_0" LIMIT ('1') :: integer) AS "done_1337";`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"insert": json.RawMessage(` [{ "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }]`), "insert": json.RawMessage(` [{ "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }]`),
@ -51,7 +49,31 @@ func bulkInsert(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
fmt.Println(">", string(resSQL)) if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func singleUpdate(t *testing.T) {
gql := `mutation {
product(id: 15, update: $update, where: { id: { eq: 1 } }) {
id
name
}
}`
sql := `test`
vars := map[string]json.RawMessage{
"update": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`),
}
resSQL, err := compileGQLToPSQL(gql, vars)
if err != nil {
t.Fatal(err)
}
fmt.Println(string(resSQL))
if string(resSQL) != sql { if string(resSQL) != sql {
t.Fatal(errNotExpected) t.Fatal(errNotExpected)
@ -61,5 +83,5 @@ func bulkInsert(t *testing.T) {
func TestCompileInsert(t *testing.T) { func TestCompileInsert(t *testing.T) {
t.Run("singleInsert", singleInsert) t.Run("singleInsert", singleInsert)
t.Run("bulkInsert", bulkInsert) t.Run("bulkInsert", bulkInsert)
t.Run("singleUpdate", singleUpdate)
} }

View File

@ -42,7 +42,7 @@ func TestMain(m *testing.M) {
"secret", "secret",
"password", "password",
"token", "token",
}, }
}) })
if err != nil { if err != nil {

View File

@ -11,12 +11,17 @@ import (
) )
type QType int type QType int
type Action int
const ( const (
maxSelectors = 30 maxSelectors = 30
QTQuery QType = iota + 1 QTQuery QType = iota + 1
QTMutation QTMutation
ActionInsert Action = iota + 1
ActionUpdate
ActionDelete
) )
type QCode struct { type QCode struct {
@ -41,6 +46,8 @@ type Select struct {
OrderBy []*OrderBy OrderBy []*OrderBy
DistinctOn []string DistinctOn []string
Paging Paging Paging Paging
Action Action
ActionVar string
Children []int32 Children []int32
} }
@ -360,6 +367,15 @@ func (com *Compiler) compileArgs(sel *Select, args []Arg) error {
err = com.compileArgLimit(sel, arg) err = com.compileArgLimit(sel, arg)
case "offset": case "offset":
err = com.compileArgOffset(sel, arg) err = com.compileArgOffset(sel, arg)
case "insert":
sel.Action = ActionInsert
err = com.compileArgAction(sel, arg)
case "update":
sel.Action = ActionUpdate
err = com.compileArgAction(sel, arg)
case "delete":
sel.Action = ActionDelete
err = com.compileArgAction(sel, arg)
} }
if err != nil { if err != nil {
@ -647,6 +663,15 @@ func (com *Compiler) compileArgOffset(sel *Select, arg *Arg) error {
return nil return nil
} }
func (com *Compiler) compileArgAction(sel *Select, arg *Arg) error {
if arg.Val.Type != nodeVar {
return fmt.Errorf("value for argument '%s' must be a variable", arg.Name)
}
sel.ActionVar = arg.Val.Val
return nil
}
func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) { func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) {
name := node.Name name := node.Name
if name[0] == '_' { if name[0] == '_' {