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
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.
@ -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
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
/*
import (
. "gopkg.in/check.v1"
)
/*
type MigrateSuite struct {
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}
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 {
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
}
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
}
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
}
@ -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'")
}
io.WriteString(c.w, ` RETURNING *) `)
root.Paging = zeroPaging
root.DistinctOn = root.DistinctOn[:]
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)
}
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]
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)
}
ti, err := c.schema.GetTable(root.Table)
if err != nil {
return 0, err
}
jt, array, err := jsn.Tree(insert)
if err != nil {
return 0, err
}
c.w.WriteString(`WITH `)
quoted(c.w, ti.Name)
c.w.WriteString(` AS (WITH "input" AS (SELECT {{`)
c.w.WriteString(`(WITH "input" AS (SELECT {{`)
c.w.WriteString(root.ActionVar)
c.w.WriteString(`}}::json AS j) INSERT INTO `)
c.w.WriteString(ti.Name)
quoted(c.w, ti.Name)
io.WriteString(c.w, ` (`)
c.renderInsertUpdateColumns(qc, w, jt, ti)
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(ti.Name)
c.w.WriteString(`, i.j) t RETURNING *) `)
c.w.WriteString(`, i.j) t`)
return 0, nil
}
@ -113,7 +122,8 @@ func (c *compilerContext) renderInsertUpdateColumns(qc *qcode.QCode, w *bytes.Bu
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]
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)
}
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 `)
quoted(c.w, ti.Name)
c.w.WriteString(` AS (WITH "input" AS (SELECT {{`)
c.w.WriteString(`(WITH "input" AS (SELECT {{`)
c.w.WriteString(root.ActionVar)
c.w.WriteString(`}}::json AS j) UPDATE `)
c.w.WriteString(ti.Name)
quoted(c.w, ti.Name)
io.WriteString(c.w, ` SET (`)
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
}
io.WriteString(c.w, ` RETURNING *) `)
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]
ti, err := c.schema.GetTable(root.Table)
if err != nil {
return 0, err
}
c.w.WriteString(`WITH `)
c.w.WriteString(`(DELETE FROM `)
quoted(c.w, ti.Name)
c.w.WriteString(` AS (DELETE FROM `)
c.w.WriteString(ti.Name)
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) 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
}

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{
"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{
"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{
"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) {
gql := `mutation {
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{
"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{
"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("bulkInsert", bulkInsert)
t.Run("singleUpdate", singleUpdate)
t.Run("singleUpsert", singleUpsert)
t.Run("bulkUpsert", bulkUpsert)
t.Run("delete", delete)
}

View File

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