Add nested insert and update mutations with support for connect and disconnect

This commit is contained in:
Vikram Rangnekar 2019-12-29 01:53:54 -05:00
parent 6831d3f56f
commit 482203ba05
10 changed files with 998 additions and 248 deletions

View File

@ -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

View File

@ -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
}
}

View File

@ -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.

View File

@ -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
}

View File

@ -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(`{

View File

@ -24,14 +24,11 @@ const (
var insertTypes = map[string]itemType{
"connect": itemConnect,
"_connect": itemConnect,
}
var updateTypes = map[string]itemType{
"connect": itemConnect,
"_connect": itemConnect,
"disconnect": itemDisconnect,
"_disconnect": itemDisconnect,
}
var noLimit = qcode.Paging{NoLimit: true}
@ -86,9 +83,12 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w io.Writer, vars Variables
type kvitem struct {
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
@ -102,10 +102,18 @@ type renitem struct {
}
func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error {
data, array, err := jsn.Tree(item.val)
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
id := item.id + 1
@ -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
}

View File

@ -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" }`),

View File

@ -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

View File

@ -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 `)
//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 {
io.WriteString(w, ` OR (`)
}
if err := renderKVItemWhere(w, v); err != nil {
return err
}
if i != 0 {
io.WriteString(w, `)`)
}
i++
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
}

View File

@ -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,16 +215,14 @@ func nestedUpdateOneToManyWithConnect(t *testing.T) {
}`),
}
for i := 0; i < 1000; i++ {
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql1 && string(resSQL) != sql2 {
if string(resSQL) != sql1 {
t.Fatal(errNotExpected)
}
}
}
func nestedUpdateOneToOneWithConnect(t *testing.T) {
@ -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)
}