Add upsert mutation

This commit is contained in:
Vikram Rangnekar 2019-10-05 02:17:08 -04:00
parent 0fc1236266
commit a1fa1f3e9e
5 changed files with 203 additions and 46 deletions

View File

@ -588,7 +588,7 @@ query {
## Mutations ## Mutations
In GraphQL mutations is the operation type for when you need to modify data. Super Graph supports the `insert`, `update` and `delete` database operations. Here are some examples. In GraphQL mutations is the operation type for when you need to modify data. Super Graph supports the `insert`, `update`, `upsert` and `delete` database operations. Here are some examples.
When using mutations the data must be passed as variables since Super Graphs compiles the query into an prepared statement in the database for maximum speed. Prepared statements are are functions in your code when called they accept arguments and your variables are passed in as those arguments. When using mutations the data must be passed as variables since Super Graphs compiles the query into an prepared statement in the database for maximum speed. Prepared statements are are functions in your code when called they accept arguments and your variables are passed in as those arguments.
@ -721,6 +721,56 @@ mutation {
} }
``` ```
### Upsert
```json
{
"data": {
"id": 5,
"name": "Art of Computer Programming",
"description": "The Art of Computer Programming (TAOCP) is a comprehensive monograph written by computer scientist Donald Knuth",
"price": 30.5
}
}
```
```graphql
mutation {
product(upsert: $data) {
id
name
}
}
```
### Bulk upsert
```json
{
"data": [{
"id": 5,
"name": "Art of Computer Programming",
"description": "The Art of Computer Programming (TAOCP) is a comprehensive monograph written by computer scientist Donald Knuth",
"price": 30.5
},
{
"id": 6,
"name": "Compilers: Principles, Techniques, and Tools",
"description": "Known to professors, students, and developers worldwide as the 'Dragon Book' is available in a new edition",
"price": 93.74
}]
}
```
```graphql
mutation {
product(upsert: $data) {
id
name
}
}
```
### Using variables ### Using variables
Variables (`$product_id`) and their values (`"product_id": 5`) can be passed along side the GraphQL query. Using variables makes for better client side code as well as improved server side SQL query caching. The build-in web-ui also supports setting variables. Not having to manipulate your GraphQL query string to insert values into it makes for cleaner Variables (`$product_id`) and their values (`"product_id": 5`) can be passed along side the GraphQL query. Using variables makes for better client side code as well as improved server side SQL query caching. The build-in web-ui also supports setting variables. Not having to manipulate your GraphQL query string to insert values into it makes for cleaner

View File

@ -1,10 +1,10 @@
package migrate_test package migrate_test
/*
import ( import (
. "gopkg.in/check.v1" . "gopkg.in/check.v1"
) )
/*
type MigrateSuite struct { type MigrateSuite struct {
conn *pgx.Conn conn *pgx.Conn

View File

@ -20,19 +20,33 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w *bytes.Buffer, vars Varia
c := &compilerContext{w, qc.Selects, co} c := &compilerContext{w, qc.Selects, co}
root := &qc.Selects[0] root := &qc.Selects[0]
ti, err := c.schema.GetTable(root.Table)
if err != nil {
return 0, err
}
c.w.WriteString(`WITH `)
quoted(c.w, ti.Name)
c.w.WriteString(` AS `)
switch root.Action { switch root.Action {
case qcode.ActionInsert: case qcode.ActionInsert:
if _, err := c.renderInsert(qc, w, vars); err != nil { if _, err := c.renderInsert(qc, w, vars, ti); err != nil {
return 0, err return 0, err
} }
case qcode.ActionUpdate: case qcode.ActionUpdate:
if _, err := c.renderUpdate(qc, w, vars); err != nil { if _, err := c.renderUpdate(qc, w, vars, ti); err != nil {
return 0, err
}
case qcode.ActionUpsert:
if _, err := c.renderUpsert(qc, w, vars, ti); err != nil {
return 0, err return 0, err
} }
case qcode.ActionDelete: case qcode.ActionDelete:
if _, err := c.renderDelete(qc, w, vars); err != nil { if _, err := c.renderDelete(qc, w, vars, ti); err != nil {
return 0, err return 0, err
} }
@ -40,6 +54,8 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w *bytes.Buffer, vars Varia
return 0, errors.New("valid mutations are 'insert' and 'update'") return 0, errors.New("valid mutations are 'insert' and 'update'")
} }
io.WriteString(c.w, ` RETURNING *) `)
root.Paging = zeroPaging root.Paging = zeroPaging
root.DistinctOn = root.DistinctOn[:] root.DistinctOn = root.DistinctOn[:]
root.OrderBy = root.OrderBy[:] root.OrderBy = root.OrderBy[:]
@ -49,7 +65,8 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w *bytes.Buffer, vars Varia
return c.compileQuery(qc, w) return c.compileQuery(qc, w)
} }
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, ti *DBTableInfo) (uint32, error) {
root := &qc.Selects[0] root := &qc.Selects[0]
insert, ok := vars[root.ActionVar] insert, ok := vars[root.ActionVar]
@ -57,23 +74,15 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w *bytes.Buffer, vars Va
return 0, fmt.Errorf("Variable '%s' not defined", root.ActionVar) 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)
if err != nil { if err != nil {
return 0, err return 0, err
} }
c.w.WriteString(`WITH `) c.w.WriteString(`(WITH "input" AS (SELECT {{`)
quoted(c.w, ti.Name)
c.w.WriteString(` AS (WITH "input" AS (SELECT {{`)
c.w.WriteString(root.ActionVar) c.w.WriteString(root.ActionVar)
c.w.WriteString(`}}::json AS j) INSERT INTO `) c.w.WriteString(`}}::json AS j) INSERT INTO `)
c.w.WriteString(ti.Name) quoted(c.w, ti.Name)
io.WriteString(c.w, ` (`) io.WriteString(c.w, ` (`)
c.renderInsertUpdateColumns(qc, w, jt, ti) c.renderInsertUpdateColumns(qc, w, jt, ti)
io.WriteString(c.w, `)`) io.WriteString(c.w, `)`)
@ -90,7 +99,7 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w *bytes.Buffer, vars Va
c.w.WriteString(`(NULL::`) c.w.WriteString(`(NULL::`)
c.w.WriteString(ti.Name) c.w.WriteString(ti.Name)
c.w.WriteString(`, i.j) t RETURNING *) `) c.w.WriteString(`, i.j) t`)
return 0, nil return 0, nil
} }
@ -113,7 +122,8 @@ func (c *compilerContext) renderInsertUpdateColumns(qc *qcode.QCode, w *bytes.Bu
return 0, nil return 0, nil
} }
func (c *compilerContext) renderUpdate(qc *qcode.QCode, w *bytes.Buffer, vars Variables) (uint32, error) { func (c *compilerContext) renderUpdate(qc *qcode.QCode, w *bytes.Buffer,
vars Variables, ti *DBTableInfo) (uint32, error) {
root := &qc.Selects[0] root := &qc.Selects[0]
update, ok := vars[root.ActionVar] update, ok := vars[root.ActionVar]
@ -121,23 +131,15 @@ func (c *compilerContext) renderUpdate(qc *qcode.QCode, w *bytes.Buffer, vars Va
return 0, fmt.Errorf("Variable '%s' not defined", root.ActionVar) 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) jt, array, err := jsn.Tree(update)
if err != nil { if err != nil {
return 0, err return 0, err
} }
c.w.WriteString(`WITH `) c.w.WriteString(`(WITH "input" AS (SELECT {{`)
quoted(c.w, ti.Name)
c.w.WriteString(` AS (WITH "input" AS (SELECT {{`)
c.w.WriteString(root.ActionVar) c.w.WriteString(root.ActionVar)
c.w.WriteString(`}}::json AS j) UPDATE `) c.w.WriteString(`}}::json AS j) UPDATE `)
c.w.WriteString(ti.Name) quoted(c.w, ti.Name)
io.WriteString(c.w, ` SET (`) io.WriteString(c.w, ` SET (`)
c.renderInsertUpdateColumns(qc, w, jt, ti) c.renderInsertUpdateColumns(qc, w, jt, ti)
@ -161,31 +163,81 @@ func (c *compilerContext) renderUpdate(qc *qcode.QCode, w *bytes.Buffer, vars Va
return 0, err return 0, err
} }
io.WriteString(c.w, ` RETURNING *) `)
return 0, nil return 0, nil
} }
func (c *compilerContext) renderDelete(qc *qcode.QCode, w *bytes.Buffer, vars Variables) (uint32, error) { func (c *compilerContext) renderDelete(qc *qcode.QCode, w *bytes.Buffer,
vars Variables, ti *DBTableInfo) (uint32, error) {
root := &qc.Selects[0] root := &qc.Selects[0]
ti, err := c.schema.GetTable(root.Table) c.w.WriteString(`(DELETE FROM `)
if err != nil {
return 0, err
}
c.w.WriteString(`WITH `)
quoted(c.w, ti.Name) quoted(c.w, ti.Name)
c.w.WriteString(` AS (DELETE FROM `)
c.w.WriteString(ti.Name)
io.WriteString(c.w, ` WHERE `) io.WriteString(c.w, ` WHERE `)
if err := c.renderWhere(root, ti); err != nil { if err := c.renderWhere(root, ti); err != nil {
return 0, err return 0, err
} }
io.WriteString(c.w, ` RETURNING *) `) return 0, nil
}
func (c *compilerContext) renderUpsert(qc *qcode.QCode, w *bytes.Buffer,
vars Variables, ti *DBTableInfo) (uint32, error) {
root := &qc.Selects[0]
upsert, ok := vars[root.ActionVar]
if !ok {
return 0, fmt.Errorf("Variable '%s' not defined", root.ActionVar)
}
jt, _, err := jsn.Tree(upsert)
if err != nil {
return 0, err
}
if _, err := c.renderInsert(qc, w, vars, ti); err != nil {
return 0, err
}
c.w.WriteString(` ON CONFLICT DO (`)
i := 0
for _, cn := range ti.ColumnNames {
if _, ok := jt[cn]; !ok {
continue
}
if col, ok := ti.Columns[cn]; !ok || !(col.UniqueKey || col.PrimaryKey) {
continue
}
if i != 0 {
io.WriteString(c.w, `, `)
}
c.w.WriteString(cn)
i++
}
if i == 0 {
c.w.WriteString(ti.PrimaryCol)
}
c.w.WriteString(`) DO `)
c.w.WriteString(`UPDATE `)
io.WriteString(c.w, ` SET `)
i = 0
for _, cn := range ti.ColumnNames {
if _, ok := jt[cn]; !ok {
continue
}
if i != 0 {
io.WriteString(c.w, `, `)
}
c.w.WriteString(cn)
io.WriteString(c.w, ` = EXCLUDED.`)
c.w.WriteString(cn)
i++
}
return 0, nil return 0, nil
} }

View File

@ -12,7 +12,7 @@ func simpleInsert(t *testing.T) {
} }
}` }`
sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO users (full_name, email) SELECT full_name, email FROM input i, json_populate_record(NULL::users, i.j) t RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id" FROM "users") AS "users_0") AS "done_1337";` sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "users" (full_name, email) SELECT full_name, email FROM input i, json_populate_record(NULL::users, i.j) t RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id" FROM "users") AS "users_0") AS "done_1337";`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`),
@ -36,7 +36,7 @@ func singleInsert(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{insert}}::json AS j) INSERT INTO products (name, description) SELECT name, description FROM input i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337";` sql := `WITH "products" AS (WITH "input" AS (SELECT {{insert}}::json AS j) INSERT INTO "products" (name, description) SELECT name, description FROM input i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") 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" }`),
@ -60,7 +60,7 @@ func bulkInsert(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{insert}}::json AS j) INSERT INTO products (name, description) SELECT name, description FROM input i, json_populate_recordset(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337";` sql := `WITH "products" AS (WITH "input" AS (SELECT {{insert}}::json AS j) INSERT INTO "products" (name, description) SELECT name, description FROM input i, json_populate_recordset(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") 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" }]`),
@ -76,6 +76,54 @@ func bulkInsert(t *testing.T) {
} }
} }
func singleUpsert(t *testing.T) {
gql := `mutation {
product(id: 15, upsert: $upsert) {
id
name
}
}`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" (name, description) SELECT name, description FROM input i, json_populate_record(NULL::products, i.j) t ON CONFLICT DO (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337";`
vars := map[string]json.RawMessage{
"upsert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`),
}
resSQL, err := compileGQLToPSQL(gql, vars)
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func bulkUpsert(t *testing.T) {
gql := `mutation {
product(id: 15, upsert: $upsert) {
id
name
}
}`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" (name, description) SELECT name, description FROM input i, json_populate_recordset(NULL::products, i.j) t ON CONFLICT DO (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337";`
vars := map[string]json.RawMessage{
"upsert": json.RawMessage(` [{ "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }]`),
}
resSQL, err := compileGQLToPSQL(gql, vars)
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func singleUpdate(t *testing.T) { func singleUpdate(t *testing.T) {
gql := `mutation { gql := `mutation {
product(id: 15, update: $update, where: { id: { eq: 1 } }) { product(id: 15, update: $update, where: { id: { eq: 1 } }) {
@ -84,7 +132,7 @@ func singleUpdate(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (WITH "input" AS (SELECT {{update}}::json AS j) UPDATE products SET (name, description) = (SELECT name, description FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."id") = 1) AND (("products"."id") = 15) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337";` sql := `WITH "products" AS (WITH "input" AS (SELECT {{update}}::json AS j) UPDATE "products" SET (name, description) = (SELECT name, description FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."id") = 1) AND (("products"."id") = 15) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337";`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"update": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), "update": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`),
@ -108,7 +156,7 @@ func delete(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (DELETE FROM products WHERE (("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."id") = 1) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337";` sql := `WITH "products" AS (DELETE FROM "products" WHERE (("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."id") = 1) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337";`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"update": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), "update": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`),
@ -129,5 +177,8 @@ 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) t.Run("singleUpdate", singleUpdate)
t.Run("singleUpsert", singleUpsert)
t.Run("bulkUpsert", bulkUpsert)
t.Run("delete", delete) t.Run("delete", delete)
} }

View File

@ -22,6 +22,7 @@ const (
ActionInsert Action = iota + 1 ActionInsert Action = iota + 1
ActionUpdate ActionUpdate
ActionDelete ActionDelete
ActionUpsert
) )
type QCode struct { type QCode struct {
@ -373,6 +374,9 @@ func (com *Compiler) compileArgs(sel *Select, args []Arg) error {
case "update": case "update":
sel.Action = ActionUpdate sel.Action = ActionUpdate
err = com.compileArgAction(sel, arg) err = com.compileArgAction(sel, arg)
case "upsert":
sel.Action = ActionUpsert
err = com.compileArgAction(sel, arg)
case "delete": case "delete":
sel.Action = ActionDelete sel.Action = ActionDelete
err = com.compileArgAction(sel, arg) err = com.compileArgAction(sel, arg)