From 482203ba05d2b46bce2358a8ad7b7e71c7999041 Mon Sep 17 00:00:00 2001 From: Vikram Rangnekar Date: Sun, 29 Dec 2019 01:53:54 -0500 Subject: [PATCH] Add nested insert and update mutations with support for connect and disconnect --- Makefile | 2 +- config/allow.list | 471 ++++++++++++++++++++++++++++++++++++++++++++ docs/guide.md | 136 ++++++++++++- psql/insert.go | 26 +-- psql/insert_test.go | 4 +- psql/mutate.go | 417 ++++++++++++++++++++++++--------------- psql/mutate_test.go | 2 +- psql/schema.go | 4 + psql/update.go | 116 +++++++---- psql/update_test.go | 68 ++++--- 10 files changed, 998 insertions(+), 248 deletions(-) diff --git a/Makefile b/Makefile index 08fe46f..854e2fa 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ changelog: $(GITCHGLOG) $(GOLANGCILINT): @GO111MODULE=off curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(GOPATH)/bin v1.21.0 -lint: $(GOMETALINTER) +lint: $(GOLANGCILINT) @golangci-lint run ./... --skip-dirs-use-default BINARY := super-graph diff --git a/config/allow.list b/config/allow.list index 1c21e21..71c4cea 100644 --- a/config/allow.list +++ b/config/allow.list @@ -281,4 +281,475 @@ mutation { } } +variables { + "update": { + "name": "my_name", + "description": "my_desc" + } +} + +mutation { + product(id: 15, update: $update, where: {id: {eq: 1}}) { + id + name + } +} + +variables { + "update": { + "name": "my_name", + "description": "my_desc" + } +} + +mutation { + product(update: $update, where: {id: {eq: 1}}) { + id + name + } +} + +variables { + "update": { + "name": "my_name 2", + "description": "my_desc 2" + } +} + +mutation { + product(update: $update, where: {id: {eq: 1}}) { + id + name + description + } +} + +variables { + "data": { + "sale_type": "tuutuu", + "quantity": 5, + "due_date": "now", + "customer": { + "email": "thedude1@rug.com", + "full_name": "The Dude" + }, + "product": { + "name": "Apple", + "price": 1.25 + } + } +} + +mutation { + purchase(update: $data, id: 5) { + sale_type + quantity + due_date + customer { + id + full_name + email + } + product { + id + name + price + } + } +} + +variables { + "data": { + "email": "thedude@rug.com", + "full_name": "The Dude", + "created_at": "now", + "updated_at": "now", + "product": { + "where": { + "id": 2 + }, + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now" + } + } +} + +mutation { + user(update: $data, where: {id: {eq: 8}}) { + id + full_name + email + product { + id + name + price + } + } +} + +variables { + "data": { + "email": "thedude@rug.com", + "full_name": "The Dude", + "created_at": "now", + "updated_at": "now", + "product": { + "where": { + "id": 2 + }, + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now" + } + } +} + +query { + user(where: {id: {eq: 8}}) { + id + product { + id + name + price + } + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now", + "user": { + "email": "thedude@rug.com" + } + } +} + +query { + user { + email + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now", + "user": { + "email": "booboo@demo.com" + } + } +} + +mutation { + product(update: $data, id: 6) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now", + "user": { + "email": "booboo@demo.com" + } + } +} + +query { + product(id: 6) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": { + "email": "thedude123@rug.com", + "full_name": "The Dude", + "created_at": "now", + "updated_at": "now", + "product": { + "connect": { + "id": 7 + }, + "disconnect": { + "id": 8 + } + } + } +} + +mutation { + user(update: $data, id: 6) { + id + full_name + email + product { + id + name + price + } + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now", + "user": { + "connect": { + "id": 5, + "email": "test@test.com" + } + } + } +} + +mutation { + product(update: $data, id: 9) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": { + "email": "thed44ude@rug.com", + "full_name": "The Dude", + "created_at": "now", + "updated_at": "now", + "product": { + "connect": { + "id": 5 + } + } + } +} + +mutation { + user(insert: $data) { + id + full_name + email + product { + id + name + price + } + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now", + "user": { + "connect": { + "id": 5 + } + } + } +} + +mutation { + product(insert: $data) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": [ + { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now", + "user": { + "connect": { + "id": 6 + } + } + }, + { + "name": "Coconut", + "price": 2.25, + "created_at": "now", + "updated_at": "now", + "user": { + "connect": { + "id": 3 + } + } + } + ] +} + +mutation { + products(insert: $data) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": [ + { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now" + }, + { + "name": "Coconut", + "price": 2.25, + "created_at": "now", + "updated_at": "now" + } + ] +} + +mutation { + products(insert: $data) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "user": { + "connect": { + "id": 5, + "email": "test@test.com" + } + } + } +} + +mutation { + product(update: $data, id: 9) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "user": { + "connect": { + "id": 5 + } + } + } +} + +mutation { + product(update: $data, id: 9) { + id + name + user { + id + full_name + email + } + } +} + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "user": { + "disconnect": { + "id": 5 + } + } + } +} + +mutation { + product(update: $data, id: 9) { + id + name + user_id + } +} + + +variables { + "data": { + "name": "Apple", + "price": 1.25, + "user": { + "disconnect": { + "id": 5 + } + } + } +} + +mutation { + product(update: $data, id: 2) { + id + name + user_id + } +} + diff --git a/docs/guide.md b/docs/guide.md index c9cd95b..b583aba 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -640,7 +640,7 @@ query { ## Mutations -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. +In GraphQL mutations is the operation type for when you need to modify data. Super Graph supports the `insert`, `update`, `upsert` and `delete`. You can also do complex nested inserts and updates. 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. @@ -823,7 +823,137 @@ mutation { } ``` -### Using variables +## Nested Mutations + +Often you will need to create or update multiple related items at the same time. This can be done using nested mutations. For example you might need to create a product and assign it to a user, or create a user and his products at the same time. You just have to use simple json to define you mutation and Super Graph takes care of the rest. + +### Nested Insert + +Create a product item first and then assign it to a user + +```json +{ + "data": { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now", + "user": { + "connect": { "id": 5 } + } + } +} +``` + +```graphql +mutation { + product(insert: $data) { + id + name + user { + id + full_name + email + } + } +} +``` + +Or it's reverse, create the user first and then his product + +```json +{ + "data": { + "email": "thedude@rug.com", + "full_name": "The Dude", + "created_at": "now", + "updated_at": "now", + "product": { + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now" + } + } +} +``` + +```graphql +mutation { + user(insert: $data) { + id + full_name + email + product { + id + name + price + } + } +} +``` + +### Nested Updates + +Update a product item first and then assign it to a user + +```json +{ + "data": { + "name": "Apple", + "price": 1.25, + "user": { + "connect": { "id": 5 } + } + } +} +``` + +```graphql +mutation { + product(update: $data, id: 5) { + id + name + user { + id + full_name + email + } + } +} +``` + +Or it's reverse, update a user first and then his product + +```json +{ + "data": { + "email": "newemail@me.com", + "full_name": "The Dude", + "product": { + "name": "Banana", + "price": 1.25, + } + } +} +``` + +```graphql +mutation { + user(update: $data, id: 1) { + id + full_name + email + product { + id + name + price + } + } +} +``` + +## 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 and better client side code. @@ -845,7 +975,7 @@ fetch('http://localhost:8080/api/v1/graphql', { .then(res => console.log(res.data)); ``` -### Full text search +## Full text search Every app these days needs search. Enought his often means reaching for something heavy like Solr. While this will work why add complexity to your infrastructure when Postgres has really great and fast full text search built-in. And since it's part of Postgres it's also available in Super Graph. diff --git a/psql/insert.go b/psql/insert.go index c65dbcb..64f7ffd 100644 --- a/psql/insert.go +++ b/psql/insert.go @@ -1,6 +1,8 @@ +//nolint:errcheck package psql import ( + "errors" "fmt" "io" @@ -27,6 +29,9 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer, if st.Len() == 0 { break } + if insert[0] == '[' && st.Len() > 1 { + return 0, errors.New("Nested bulk insert not supported") + } intf := st.Pop() switch item := intf.(type) { @@ -38,8 +43,6 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer, case renitem: var err error - io.WriteString(c.w, `, `) - // if w := qc.Selects[0].Where; w != nil && w.Op == qcode.OpFalse { // io.WriteString(c.w, ` WHERE false`) // } @@ -50,7 +53,7 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer, case itemConnect: err = c.renderConnectStmt(qc, w, item) case itemUnion: - err = c.renderInsertUnionStmt(w, item) + err = c.renderUnionStmt(w, item) } if err != nil { @@ -69,6 +72,7 @@ func (c *compilerContext) renderInsertStmt(qc *qcode.QCode, w io.Writer, item re jt := item.data sk := nestedInsertRelColumnsMap(item.kvitem) + io.WriteString(c.w, `, `) renderCteName(w, item.kvitem) io.WriteString(w, ` AS (`) @@ -171,19 +175,3 @@ func renderNestedInsertRelTables(w io.Writer, item kvitem) error { return nil } - -func (c *compilerContext) renderInsertUnionStmt(w io.Writer, item renitem) error { - renderCteName(w, item.kvitem) - io.WriteString(w, ` AS (`) - - for i, v := range item.items { - if i != 0 { - io.WriteString(w, ` UNION ALL `) - } - io.WriteString(w, `SELECT * FROM `) - renderCteName(w, v) - } - io.WriteString(w, `)`) - - return nil -} diff --git a/psql/insert_test.go b/psql/insert_test.go index cbf971f..e607930 100644 --- a/psql/insert_test.go +++ b/psql/insert_test.go @@ -249,7 +249,7 @@ func nestedInsertOneToManyWithConnect(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *), "products_2" AS (UPDATE "products" SET "user_id" = "users"."id" WHERE "id" = '5' RETURNING *), "products" AS (SELECT * FROM "products_2") SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *), "products" AS ( UPDATE "products" SET "user_id" = "users"."id"FROM "users" WHERE ("products"."id" = '5') RETURNING "products".*) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{ @@ -286,7 +286,7 @@ func nestedInsertOneToOneWithConnect(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users_2" AS (SELECT * FROM "users" WHERE "id" = '5' LIMIT 1 RETURNING *), "users" AS (SELECT * FROM "users_2"), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM "users" WHERE "users"."id" = '5' LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{ diff --git a/psql/mutate.go b/psql/mutate.go index b40b9d6..0afc7f0 100644 --- a/psql/mutate.go +++ b/psql/mutate.go @@ -23,15 +23,12 @@ const ( ) var insertTypes = map[string]itemType{ - "connect": itemConnect, - "_connect": itemConnect, + "connect": itemConnect, } var updateTypes = map[string]itemType{ - "connect": itemConnect, - "_connect": itemConnect, - "disconnect": itemDisconnect, - "_disconnect": itemDisconnect, + "connect": itemConnect, + "disconnect": itemDisconnect, } var noLimit = qcode.Paging{NoLimit: true} @@ -84,15 +81,18 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w io.Writer, vars Variables } type kvitem struct { - id int32 - _type itemType - key string - path []string - val json.RawMessage - ti *DBTableInfo - relCP *DBRel - relPC *DBRel - items []kvitem + id int32 + _type itemType + _ctype int + key string + path []string + val json.RawMessage + data map[string]json.RawMessage + array bool + ti *DBTableInfo + relCP *DBRel + relPC *DBRel + items []kvitem } type renitem struct { @@ -102,9 +102,17 @@ type renitem struct { } func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error { - data, array, err := jsn.Tree(item.val) - if err != nil { - return err + var data map[string]json.RawMessage + var array bool + var err error + + if item.data == nil { + data, array, err = jsn.Tree(item.val) + if err != nil { + return err + } + } else { + data, array = item.data, item.array } var unionize bool @@ -155,7 +163,7 @@ func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error { return err } - item.items = append(item.items, kvitem{ + item1 := kvitem{ id: id, _type: item._type, key: k, @@ -164,7 +172,22 @@ func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error { ti: ti, relCP: relCP, relPC: relPC, - }) + } + + if v[0] == '{' { + item1.data, item1.array, err = jsn.Tree(v) + if err != nil { + return err + } + if v1, ok := item1.data["connect"]; ok && (v1[0] == '{' || v1[0] == '[') { + item1._ctype |= (1 << itemConnect) + } + if v1, ok := item1.data["disconnect"]; ok && (v1[0] == '{' || v1[0] == '[') { + item1._ctype |= (1 << itemDisconnect) + } + } + + item.items = append(item.items, item1) id++ } } @@ -194,11 +217,25 @@ func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error { } } + case itemUpdate: + for _, v := range item.items { + if !(v._ctype > 0 && v.relPC.Type == RelOneToOne) { + st.Push(v) + } + } + st.Push(renitem{kvitem: item, array: array, data: data}) + for _, v := range item.items { + if v._ctype > 0 && v.relPC.Type == RelOneToOne { + st.Push(v) + } + } + case itemUnion: st.Push(renitem{kvitem: item, array: array, data: data}) for _, v := range item.items { st.Push(v) } + default: for _, v := range item.items { st.Push(v) @@ -209,6 +246,112 @@ func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error { return nil } +func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error { + var connect, disconnect bool + + // Render only for parent-to-child relationship of one-to-many + if item.relPC.Type != RelOneToMany { + return nil + } + + for _, v := range item.items { + if v._type == itemConnect { + connect = true + } else if v._type == itemDisconnect { + disconnect = true + } + if connect && disconnect { + break + } + } + + if connect { + io.WriteString(w, `, `) + if connect && disconnect { + renderCteNameWithSuffix(w, item.kvitem, "c") + } else { + quoted(w, item.ti.Name) + } + io.WriteString(w, ` AS ( UPDATE `) + quoted(w, item.ti.Name) + io.WriteString(w, ` SET `) + quoted(w, item.relPC.Right.Col) + io.WriteString(w, ` = `) + colWithTable(w, item.relPC.Left.Table, item.relPC.Left.Col) + io.WriteString(w, `FROM `) + quoted(w, item.relPC.Left.Table) + io.WriteString(w, ` WHERE`) + + i := 0 + for _, v := range item.items { + if v._type == itemConnect { + if i != 0 { + io.WriteString(w, ` OR (`) + } else { + io.WriteString(w, ` (`) + } + if err := renderKVItemWhere(w, v); err != nil { + return err + } + io.WriteString(w, `)`) + i++ + } + } + io.WriteString(w, ` RETURNING `) + quoted(w, item.ti.Name) + io.WriteString(w, `.*)`) + } + + if disconnect { + io.WriteString(w, `, `) + if connect && disconnect { + renderCteNameWithSuffix(w, item.kvitem, "d") + } else { + quoted(w, item.ti.Name) + } + io.WriteString(w, ` AS ( UPDATE `) + quoted(w, item.ti.Name) + io.WriteString(w, ` SET `) + quoted(w, item.relPC.Right.Col) + io.WriteString(w, ` = NULL`) + io.WriteString(w, ` FROM `) + quoted(w, item.relPC.Left.Table) + io.WriteString(w, ` WHERE`) + + i := 0 + for _, v := range item.items { + if v._type == itemDisconnect { + if i != 0 { + io.WriteString(w, ` OR (`) + } else { + io.WriteString(w, ` (`) + } + if err := renderKVItemWhere(w, v); err != nil { + return err + } + io.WriteString(w, `)`) + i++ + } + } + io.WriteString(w, ` RETURNING `) + quoted(w, item.ti.Name) + io.WriteString(w, `.*), `) + } + + if connect && disconnect { + quoted(w, item.ti.Name) + io.WriteString(w, ` AS (`) + io.WriteString(w, `SELECT * FROM `) + renderCteNameWithSuffix(w, item.kvitem, "c") + io.WriteString(w, ` UNION ALL `) + io.WriteString(w, `SELECT * FROM `) + renderCteNameWithSuffix(w, item.kvitem, "d") + io.WriteString(w, `)`) + } + + return nil +} + func renderInsertUpdateColumns(w io.Writer, qc *qcode.QCode, jt map[string]json.RawMessage, @@ -346,6 +489,101 @@ func (c *compilerContext) renderUpsert(qc *qcode.QCode, w io.Writer, return 0, nil } +func (c *compilerContext) renderConnectStmt(qc *qcode.QCode, w io.Writer, + item renitem) error { + + rel := item.relPC + + // Render only for parent-to-child relationship of one-to-one + if rel.Type != RelOneToOne { + return nil + } + + io.WriteString(w, `, `) + quoted(w, item.ti.Name) + io.WriteString(c.w, ` AS (`) + + io.WriteString(c.w, `SELECT * FROM `) + quoted(c.w, item.ti.Name) + io.WriteString(c.w, ` WHERE `) + if err := renderKVItemWhere(c.w, item.kvitem); err != nil { + return err + } + io.WriteString(c.w, ` LIMIT 1)`) + + return nil +} + +func (c *compilerContext) renderDisconnectStmt(qc *qcode.QCode, w io.Writer, + item renitem) error { + + rel := item.relPC + + // Render only for parent-to-child relationship of one-to-one + if rel.Type != RelOneToOne { + return nil + } + io.WriteString(w, `, `) + quoted(w, item.ti.Name) + io.WriteString(c.w, ` AS (`) + + io.WriteString(c.w, `SELECT * FROM (VALUES(NULL::`) + io.WriteString(w, rel.Right.col.Type) + io.WriteString(c.w, `)) AS LOOKUP(`) + quoted(w, rel.Right.Col) + io.WriteString(c.w, `))`) + + return nil +} + +func renderKVItemWhere(w io.Writer, item kvitem) error { + return renderWhereFromJSON(w, item.ti.Name, item.val) +} + +func renderWhereFromJSON(w io.Writer, table string, val []byte) error { + var kv map[string]json.RawMessage + if err := json.Unmarshal(val, &kv); err != nil { + return err + } + i := 0 + for k, v := range kv { + if i != 0 { + io.WriteString(w, ` AND `) + } + colWithTable(w, table, k) + io.WriteString(w, ` = '`) + switch v[0] { + case '"': + w.Write(v[1 : len(v)-1]) + default: + w.Write(v) + } + io.WriteString(w, `'`) + i++ + } + return nil +} + +func renderCteName(w io.Writer, item kvitem) error { + io.WriteString(w, `"`) + io.WriteString(w, item.ti.Name) + if item._type == itemConnect || item._type == itemDisconnect { + io.WriteString(w, `_`) + int2string(w, item.id) + } + io.WriteString(w, `"`) + return nil +} + +func renderCteNameWithSuffix(w io.Writer, item kvitem, suffix string) error { + io.WriteString(w, `"`) + io.WriteString(w, item.ti.Name) + io.WriteString(w, `_`) + io.WriteString(w, suffix) + io.WriteString(w, `"`) + return nil +} + func quoted(w io.Writer, identifier string) { io.WriteString(w, `"`) io.WriteString(w, identifier) @@ -362,142 +600,3 @@ func joinPath(w io.Writer, path []string) { io.WriteString(w, `'`) } } - -func (c *compilerContext) renderConnectStmt(qc *qcode.QCode, w io.Writer, - item renitem) error { - - rel := item.relPC - - renderCteName(c.w, item.kvitem) - io.WriteString(c.w, ` AS (`) - - // Render either select or update sql based on parent-to-child - // relationship - switch rel.Type { - case RelOneToOne: - io.WriteString(c.w, `SELECT * FROM `) - quoted(c.w, item.ti.Name) - io.WriteString(c.w, ` WHERE `) - if err := renderKVItemWhere(c.w, item.kvitem); err != nil { - return err - } - io.WriteString(c.w, ` LIMIT 1`) - - case RelOneToMany: - // UPDATE films SET kind = 'Dramatic' WHERE kind = 'Drama'; - io.WriteString(c.w, `UPDATE `) - quoted(c.w, item.ti.Name) - io.WriteString(c.w, ` SET `) - quoted(c.w, rel.Right.Col) - io.WriteString(c.w, ` = `) - colWithTable(c.w, rel.Left.Table, rel.Left.Col) - io.WriteString(c.w, ` WHERE `) - if err := renderKVItemWhere(c.w, item.kvitem); err != nil { - return err - } - - default: - return fmt.Errorf("unsuppported relationship %s", rel) - } - - io.WriteString(c.w, ` RETURNING *)`) - - return nil - -} - -func (c *compilerContext) renderDisconnectStmt(qc *qcode.QCode, w io.Writer, - item renitem) error { - - renderCteName(c.w, item.kvitem) - io.WriteString(c.w, ` AS (`) - - io.WriteString(c.w, `UPDATE `) - quoted(c.w, item.ti.Name) - io.WriteString(c.w, ` SET `) - quoted(c.w, item.relPC.Right.Col) - io.WriteString(c.w, ` = NULL `) - io.WriteString(c.w, ` WHERE `) - - // Render either select or update sql based on parent-to-child - // relationship - switch item.relPC.Type { - case RelOneToOne: - if err := renderRelEquals(c.w, item.relPC); err != nil { - return err - } - - case RelOneToMany: - if err := renderRelEquals(c.w, item.relPC); err != nil { - return err - } - - io.WriteString(c.w, ` AND `) - - if err := renderKVItemWhere(c.w, item.kvitem); err != nil { - return err - } - - default: - return fmt.Errorf("unsuppported relationship %s", item.relPC) - } - - io.WriteString(c.w, ` RETURNING *)`) - - return nil -} - -func renderKVItemWhere(w io.Writer, item kvitem) error { - return renderWhereFromJSON(w, item.val) -} - -func renderWhereFromJSON(w io.Writer, val []byte) error { - var kv map[string]json.RawMessage - if err := json.Unmarshal(val, &kv); err != nil { - return err - } - i := 0 - for k, v := range kv { - if i != 0 { - io.WriteString(w, ` AND `) - } - quoted(w, k) - io.WriteString(w, ` = '`) - switch v[0] { - case '"': - w.Write(v[1 : len(v)-1]) - default: - w.Write(v) - } - io.WriteString(w, `'`) - i++ - } - return nil -} - -func renderRelEquals(w io.Writer, rel *DBRel) error { - switch rel.Type { - case RelOneToOne: - colWithTable(w, rel.Left.Table, rel.Left.Col) - io.WriteString(w, ` = `) - colWithTable(w, rel.Right.Table, rel.Right.Col) - - case RelOneToMany: - colWithTable(w, rel.Right.Table, rel.Right.Col) - io.WriteString(w, ` = `) - colWithTable(w, rel.Left.Table, rel.Left.Col) - } - - return nil -} - -func renderCteName(w io.Writer, item kvitem) error { - io.WriteString(w, `"`) - io.WriteString(w, item.ti.Name) - if item._type == itemConnect || item._type == itemDisconnect { - io.WriteString(w, `_`) - int2string(w, item.id) - } - io.WriteString(w, `"`) - return nil -} diff --git a/psql/mutate_test.go b/psql/mutate_test.go index 862df9d..7f498a9 100644 --- a/psql/mutate_test.go +++ b/psql/mutate_test.go @@ -85,7 +85,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', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` + sql := `WITH "products" AS (DELETE FROM "products" WHERE (("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."id") = 1) RETURNING "products".*)SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "update": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`), diff --git a/psql/schema.go b/psql/schema.go index e98d7eb..78a7b5d 100644 --- a/psql/schema.go +++ b/psql/schema.go @@ -38,11 +38,13 @@ type DBRel struct { Through string ColT string Left struct { + col *DBColumn Table string Col string Array bool } Right struct { + col *DBColumn Table string Col string Array bool @@ -166,10 +168,12 @@ func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error { rel1 = &DBRel{Type: RelOneToMany} } + rel1.Left.col = &c rel1.Left.Table = t.Name rel1.Left.Col = c.Name rel1.Left.Array = c.Array + rel1.Right.col = fc rel1.Right.Table = c.FKeyTable rel1.Right.Col = fc.Name rel1.Right.Array = fc.Array diff --git a/psql/update.go b/psql/update.go index 2c458f2..686367b 100644 --- a/psql/update.go +++ b/psql/update.go @@ -1,6 +1,8 @@ +//nolint:errcheck package psql import ( + "errors" "fmt" "io" @@ -11,7 +13,7 @@ import ( func (c *compilerContext) renderUpdate(qc *qcode.QCode, w io.Writer, vars Variables, ti *DBTableInfo) (uint32, error) { - insert, ok := vars[qc.ActionVar] + update, ok := vars[qc.ActionVar] if !ok { return 0, fmt.Errorf("Variable '%s' not !defined", qc.ActionVar) } @@ -21,12 +23,15 @@ func (c *compilerContext) renderUpdate(qc *qcode.QCode, w io.Writer, io.WriteString(c.w, `}}' :: json AS j)`) st := util.NewStack() - st.Push(kvitem{_type: itemUpdate, key: ti.Name, val: insert, ti: ti}) + st.Push(kvitem{_type: itemUpdate, key: ti.Name, val: update, ti: ti}) for { if st.Len() == 0 { break } + if update[0] == '[' && st.Len() > 1 { + return 0, errors.New("Nested bulk update not supported") + } intf := st.Pop() switch item := intf.(type) { @@ -45,12 +50,12 @@ func (c *compilerContext) renderUpdate(qc *qcode.QCode, w io.Writer, switch item._type { case itemUpdate: err = c.renderUpdateStmt(w, qc, item) - // case itemConnect: - // err = c.renderConnectStmt(qc, w, item) - // case itemDisconnect: - // err = c.renderDisconnectStmt(qc, w, item) + case itemConnect: + err = c.renderConnectStmt(qc, w, item) + case itemDisconnect: + err = c.renderDisconnectStmt(qc, w, item) case itemUnion: - err = c.renderUpdateUnionStmt(w, item) + err = c.renderUnionStmt(w, item) } if err != nil { @@ -67,6 +72,7 @@ func (c *compilerContext) renderUpdate(qc *qcode.QCode, w io.Writer, func (c *compilerContext) renderUpdateStmt(w io.Writer, qc *qcode.QCode, item renitem) error { ti := item.ti jt := item.data + sk := nestedUpdateRelColumnsMap(item.kvitem) io.WriteString(c.w, `, `) renderCteName(c.w, item.kvitem) @@ -75,12 +81,15 @@ func (c *compilerContext) renderUpdateStmt(w io.Writer, qc *qcode.QCode, item re io.WriteString(w, `UPDATE `) quoted(w, ti.Name) io.WriteString(w, ` SET (`) - renderInsertUpdateColumns(w, qc, jt, ti, nil, false) + renderInsertUpdateColumns(w, qc, jt, ti, sk, false) + renderNestedUpdateRelColumns(w, item.kvitem, false) io.WriteString(w, `) = (SELECT `) - renderInsertUpdateColumns(w, qc, jt, ti, nil, true) + renderInsertUpdateColumns(w, qc, jt, ti, sk, true) + renderNestedUpdateRelColumns(w, item.kvitem, true) io.WriteString(w, ` FROM "_sg_input" i, `) + renderNestedUpdateRelTables(w, item.kvitem) if item.array { io.WriteString(w, `json_populate_recordset`) @@ -90,15 +99,24 @@ func (c *compilerContext) renderUpdateStmt(w io.Writer, qc *qcode.QCode, item re io.WriteString(w, `(NULL::`) io.WriteString(w, ti.Name) - io.WriteString(w, `, i.j) t`) - io.WriteString(w, ` WHERE `) + if len(item.path) == 0 { + io.WriteString(w, `, i.j) t)`) + } else { + io.WriteString(w, `, i.j->`) + joinPath(w, item.path) + io.WriteString(w, `) t) `) + } if item.id != 0 { // Render sql to set id values if child-to-parent // relationship is one-to-one rel := item.relCP - io.WriteString(w, `((`) + + io.WriteString(w, `FROM `) + quoted(w, rel.Right.Table) + + io.WriteString(w, ` WHERE ((`) colWithTable(w, rel.Left.Table, rel.Left.Col) io.WriteString(w, `) = (`) colWithTable(w, rel.Right.Table, rel.Right.Col) @@ -107,53 +125,68 @@ func (c *compilerContext) renderUpdateStmt(w io.Writer, qc *qcode.QCode, item re if item.relPC.Type == RelOneToMany { if conn, ok := item.data["where"]; ok { io.WriteString(w, ` AND `) - renderWhereFromJSON(w, conn) + renderWhereFromJSON(w, item.ti.Name, conn) } else if conn, ok := item.data["_where"]; ok { io.WriteString(w, ` AND `) - renderWhereFromJSON(w, conn) + renderWhereFromJSON(w, item.ti.Name, conn) } } io.WriteString(w, `)`) } else { + io.WriteString(w, `WHERE `) if err := c.renderWhere(&qc.Selects[0], ti); err != nil { return err } } - io.WriteString(w, `) RETURNING *)`) + io.WriteString(w, ` RETURNING `) + quoted(w, ti.Name) + io.WriteString(w, `.*)`) return nil } -func (c *compilerContext) renderUpdateUnionStmt(w io.Writer, item renitem) error { - renderCteName(w, item.kvitem) - io.WriteString(w, ` AS (`) +func nestedUpdateRelColumnsMap(item kvitem) map[string]struct{} { + sk := make(map[string]struct{}, len(item.items)) - i := 0 for _, v := range item.items { - if v._type == itemConnect { - if i == 0 { - io.WriteString(w, `UPDATE `) - quoted(w, v.ti.Name) - io.WriteString(w, ` SET `) - quoted(w, v.relPC.Right.Col) - io.WriteString(w, ` = `) - colWithTable(w, v.relPC.Left.Table, v.relPC.Left.Col) - io.WriteString(w, ` WHERE `) - } else { - io.WriteString(w, ` OR (`) - } - if err := renderKVItemWhere(w, v); err != nil { - return err - } - if i != 0 { - io.WriteString(w, `)`) - } - i++ + //fmt.Println(">>", v._ctype > 0 && v.relCP.Type == RelOneToMany, v.relCP.Right.Col) + + if v._ctype > 0 && v.relCP.Type == RelOneToMany { + sk[v.relCP.Right.Col] = struct{}{} + } + } + + return sk +} + +func renderNestedUpdateRelColumns(w io.Writer, item kvitem, values bool) error { + // Render child foreign key columns if child-to-parent + // relationship is one-to-many + for _, v := range item.items { + if v._ctype > 0 && v.relCP.Type == RelOneToMany { + io.WriteString(w, `, `) + if values { + colWithTable(w, v.relCP.Left.Table, v.relCP.Left.Col) + } else { + quoted(w, v.relCP.Right.Col) + } + } + } + + return nil +} + +func renderNestedUpdateRelTables(w io.Writer, item kvitem) error { + // Render child foreign key columns if child-to-parent + // relationship is one-to-many + for _, v := range item.items { + if v._ctype > 0 && v.relCP.Type == RelOneToMany { + quoted(w, v.relCP.Left.Table) + io.WriteString(w, `, `) } } - io.WriteString(w, `)`) return nil } @@ -173,7 +206,8 @@ func (c *compilerContext) renderDelete(qc *qcode.QCode, w io.Writer, return 0, err } - io.WriteString(c.w, ` RETURNING *) `) - + io.WriteString(w, ` RETURNING `) + quoted(w, ti.Name) + io.WriteString(w, `.*)`) return 0, nil } diff --git a/psql/update_test.go b/psql/update_test.go index 02ffa34..b621888 100644 --- a/psql/update_test.go +++ b/psql/update_test.go @@ -2,7 +2,6 @@ package psql import ( "encoding/json" - "fmt" "testing" ) @@ -14,7 +13,7 @@ func singleUpdate(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{update}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "description") = (SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t WHERE (("products"."id") = 1) AND (("products"."id") = 15)) RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` + sql := `WITH "_sg_input" AS (SELECT '{{update}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "description") = (SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t)WHERE (("products"."id") = 1) AND (("products"."id") = 15) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "update": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`), @@ -37,7 +36,7 @@ func simpleUpdateWithPresets(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t WHERE (("products"."user_id") = '{{user_id}}' :: bigint)) RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` + sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t)WHERE (("products"."user_id") = '{{user_id}}' :: bigint) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{"name": "Apple", "price": 1.25}`), @@ -72,9 +71,9 @@ func nestedUpdateManyToMany(t *testing.T) { } }` - sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t WHERE (("purchases"."id") = 5)) RETURNING *), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j) t WHERE (("customers"."id") = ("purchases"."customer_id"))) RETURNING *), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t WHERE (("products"."id") = ("purchases"."product_id"))) RETURNING *) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t)WHERE (("purchases"."id") = 5) RETURNING "purchases".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` - sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t WHERE (("purchases"."id") = 5)) RETURNING *), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t WHERE (("products"."id") = ("purchases"."product_id"))) RETURNING *), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j) t WHERE (("customers"."id") = ("purchases"."customer_id"))) RETURNING *) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t)WHERE (("purchases"."id") = 5) RETURNING "purchases".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(` { @@ -120,7 +119,7 @@ func nestedUpdateOneToMany(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t WHERE (("users"."id") = 8)) RETURNING *), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t WHERE (("products"."user_id") = ("users"."id") AND "id" = '2')) RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t)WHERE (("users"."id") = 8) RETURNING "users".*), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "users" WHERE (("products"."user_id") = ("users"."id") AND "products"."id" = '2') RETURNING "products".*) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{ @@ -163,7 +162,7 @@ func nestedUpdateOneToOne(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t WHERE (("products"."id") = 6)) RETURNING *), "users" AS (UPDATE "users" SET ("email") = (SELECT "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t WHERE (("users"."id") = ("products"."user_id"))) RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t)WHERE (("products"."id") = 6) RETURNING "products".*), "users" AS (UPDATE "users" SET ("email") = (SELECT "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j->'user') t) FROM "products" WHERE (("users"."id") = ("products"."user_id")) RETURNING "users".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{ @@ -201,9 +200,7 @@ func nestedUpdateOneToManyWithConnect(t *testing.T) { } }` - sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t WHERE (("users"."id") = 6)) RETURNING *), "products_3" AS (UPDATE "products" SET "user_id" = NULL WHERE "products"."user_id" = "users"."id" AND "id" = '8' RETURNING *), "products_2" AS (UPDATE "products" SET "user_id" = "users"."id" WHERE "id" = '7' RETURNING *), "products" AS (SELECT * FROM "products_2" UNION ALL SELECT * FROM "products_3") SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` - - sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t WHERE (("users"."id") = 6)) RETURNING *), "products_3" AS (UPDATE "products" SET "user_id" = "users"."id" WHERE "id" = '7' RETURNING *), "products_2" AS (UPDATE "products" SET "user_id" = NULL WHERE "products"."user_id" = "users"."id" AND "id" = '8' RETURNING *), "products" AS (SELECT * FROM "products_2" UNION ALL SELECT * FROM "products_3") SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t)WHERE (("users"."id") = 6) RETURNING "users".*), "products_c" AS ( UPDATE "products" SET "user_id" = "users"."id"FROM "users" WHERE ("products"."id" = '7') RETURNING "products".*), "products_d" AS ( UPDATE "products" SET "user_id" = NULL FROM "users" WHERE ("products"."id" = '8') RETURNING "products".*), "products" AS (SELECT * FROM "products_c" UNION ALL SELECT * FROM "products_d") SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{ @@ -218,15 +215,13 @@ func nestedUpdateOneToManyWithConnect(t *testing.T) { }`), } - for i := 0; i < 1000; i++ { - resSQL, err := compileGQLToPSQL(gql, vars, "admin") - if err != nil { - t.Fatal(err) - } + resSQL, err := compileGQLToPSQL(gql, vars, "admin") + if err != nil { + t.Fatal(err) + } - if string(resSQL) != sql1 && string(resSQL) != sql2 { - t.Fatal(errNotExpected) - } + if string(resSQL) != sql1 { + t.Fatal(errNotExpected) } } @@ -243,14 +238,12 @@ func nestedUpdateOneToOneWithConnect(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM "users" WHERE "id" = '5' AND "email" = 'test@test.com' LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at", "user_id") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t WHERE (("products"."id") = 9)) RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM "users" WHERE "users"."id" = '5' AND "users"."email" = 'test@test.com' LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t)WHERE (("products"."id") = 9) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{ "name": "Apple", "price": 1.25, - "created_at": "now", - "updated_at": "now", "user": { "connect": { "id": 5, "email": "test@test.com" } } @@ -261,7 +254,37 @@ func nestedUpdateOneToOneWithConnect(t *testing.T) { if err != nil { t.Fatal(err) } - fmt.Println(string(resSQL)) + + if string(resSQL) != sql { + t.Fatal(errNotExpected) + } +} + +func nestedUpdateOneToOneWithDisconnect(t *testing.T) { + gql := `mutation { + product(update: $data, id: 2) { + id + name + user_id + } + }` + + sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM (VALUES(NULL::bigint)) AS LOOKUP("id")), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t)WHERE (("products"."id") = 2) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."user_id" AS "user_id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` + + vars := map[string]json.RawMessage{ + "data": json.RawMessage(`{ + "name": "Apple", + "price": 1.25, + "user": { + "disconnect": { "id": 5 } + } + }`), + } + + resSQL, err := compileGQLToPSQL(gql, vars, "admin") + if err != nil { + t.Fatal(err) + } if string(resSQL) != sql { t.Fatal(errNotExpected) @@ -276,4 +299,5 @@ func TestCompileUpdate(t *testing.T) { t.Run("nestedUpdateOneToOne", nestedUpdateOneToOne) t.Run("nestedUpdateOneToManyWithConnect", nestedUpdateOneToManyWithConnect) t.Run("nestedUpdateOneToOneWithConnect", nestedUpdateOneToOneWithConnect) + t.Run("nestedUpdateOneToOneWithDisconnect", nestedUpdateOneToOneWithDisconnect) }