Add nested mutations

This commit is contained in:
Vikram Rangnekar 2019-12-25 01:24:30 -05:00
parent 96ed3413fc
commit 6831d3f56f
23 changed files with 1617 additions and 404 deletions

View File

@ -182,4 +182,103 @@ query beerSearch {
} }
} }
query {
user {
id
full_name
}
}
variables {
"data": {
"email": "goo1@rug.com",
"full_name": "The Dude",
"created_at": "now",
"updated_at": "now",
"product": {
"name": "Apple",
"price": 1.25,
"created_at": "now",
"updated_at": "now"
}
}
}
mutation {
user(insert: $data) {
id
full_name
email
product {
id
name
price
}
}
}
variables {
"data": {
"email": "goo12@rug.com",
"full_name": "The Dude",
"created_at": "now",
"updated_at": "now",
"product": [
{
"name": "Banana 1",
"price": 1.1,
"created_at": "now",
"updated_at": "now"
},
{
"name": "Banana 2",
"price": 2.2,
"created_at": "now",
"updated_at": "now"
}
]
}
}
mutation {
user(insert: $data) {
id
full_name
email
products {
id
name
price
}
}
}
variables {
"data": {
"name": "Banana 3",
"price": 1.1,
"created_at": "now",
"updated_at": "now",
"user": {
"email": "a2@a.com",
"full_name": "The Dude",
"created_at": "now",
"updated_at": "now"
}
}
}
mutation {
products(insert: $data) {
id
name
price
user {
id
full_name
email
}
}
}

View File

@ -37,6 +37,6 @@ services:
command: wtc command: wtc
depends_on: depends_on:
- db - db
- rails_app # - rails_app
# - redis # - redis

View File

@ -5,7 +5,7 @@ import (
"encoding/json" "encoding/json"
) )
func Tree(v []byte) (map[string]interface{}, bool, error) { func Tree(v []byte) (map[string]json.RawMessage, bool, error) {
dec := json.NewDecoder(bytes.NewReader(v)) dec := json.NewDecoder(bytes.NewReader(v))
array := false array := false
@ -25,7 +25,7 @@ func Tree(v []byte) (map[string]interface{}, bool, error) {
} }
// while the array contains values // while the array contains values
var m map[string]interface{} var m map[string]json.RawMessage
// decode an array value (Message) // decode an array value (Message)
err := dec.Decode(&m) err := dec.Decode(&m)

189
psql/insert.go Normal file
View File

@ -0,0 +1,189 @@
package psql
import (
"fmt"
"io"
"github.com/dosco/super-graph/qcode"
"github.com/dosco/super-graph/util"
)
func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer,
vars Variables, ti *DBTableInfo) (uint32, error) {
insert, ok := vars[qc.ActionVar]
if !ok {
return 0, fmt.Errorf("Variable '%s' not !defined", qc.ActionVar)
}
io.WriteString(c.w, `WITH "_sg_input" AS (SELECT '{{`)
io.WriteString(c.w, qc.ActionVar)
io.WriteString(c.w, `}}' :: json AS j)`)
st := util.NewStack()
st.Push(kvitem{_type: itemInsert, key: ti.Name, val: insert, ti: ti})
for {
if st.Len() == 0 {
break
}
intf := st.Pop()
switch item := intf.(type) {
case kvitem:
if err := c.handleKVItem(st, item); err != nil {
return 0, err
}
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`)
// }
switch item._type {
case itemInsert:
err = c.renderInsertStmt(qc, w, item)
case itemConnect:
err = c.renderConnectStmt(qc, w, item)
case itemUnion:
err = c.renderInsertUnionStmt(w, item)
}
if err != nil {
return 0, err
}
}
}
io.WriteString(c.w, ` `)
return 0, nil
}
func (c *compilerContext) renderInsertStmt(qc *qcode.QCode, w io.Writer, item renitem) error {
ti := item.ti
jt := item.data
sk := nestedInsertRelColumnsMap(item.kvitem)
renderCteName(w, item.kvitem)
io.WriteString(w, ` AS (`)
io.WriteString(w, `INSERT INTO `)
quoted(w, ti.Name)
io.WriteString(w, ` (`)
renderInsertUpdateColumns(w, qc, jt, ti, sk, false)
renderNestedInsertRelColumns(w, item.kvitem, false)
io.WriteString(w, `)`)
io.WriteString(w, ` SELECT `)
renderInsertUpdateColumns(w, qc, jt, ti, sk, true)
renderNestedInsertRelColumns(w, item.kvitem, true)
io.WriteString(w, ` FROM "_sg_input" i, `)
renderNestedInsertRelTables(w, item.kvitem)
if item.array {
io.WriteString(w, `json_populate_recordset`)
} else {
io.WriteString(w, `json_populate_record`)
}
io.WriteString(w, `(NULL::`)
io.WriteString(w, ti.Name)
if len(item.path) == 0 {
io.WriteString(w, `, i.j) t RETURNING *)`)
} else {
io.WriteString(w, `, i.j->`)
joinPath(w, item.path)
io.WriteString(w, `) t RETURNING *)`)
}
return nil
}
func nestedInsertRelColumnsMap(item kvitem) map[string]struct{} {
sk := make(map[string]struct{}, len(item.items))
if len(item.items) == 0 {
if item.relPC != nil && item.relPC.Type == RelOneToMany {
sk[item.relPC.Right.Col] = struct{}{}
}
} else {
for _, v := range item.items {
if v.relCP.Type == RelOneToMany {
sk[v.relCP.Right.Col] = struct{}{}
}
}
}
return sk
}
func renderNestedInsertRelColumns(w io.Writer, item kvitem, values bool) error {
if len(item.items) == 0 {
if item.relPC != nil && item.relPC.Type == RelOneToMany {
io.WriteString(w, `, `)
if values {
colWithTable(w, item.relPC.Left.Table, item.relPC.Left.Col)
} else {
quoted(w, item.relPC.Right.Col)
}
}
} else {
// Render child foreign key columns if child-to-parent
// relationship is one-to-many
for _, v := range item.items {
if 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 renderNestedInsertRelTables(w io.Writer, item kvitem) error {
if len(item.items) == 0 {
if item.relPC != nil && item.relPC.Type == RelOneToMany {
quoted(w, item.relPC.Left.Table)
io.WriteString(w, `, `)
}
} else {
// Render child foreign key columns if child-to-parent
// relationship is one-to-many
for _, v := range item.items {
if v.relCP.Type == RelOneToMany {
quoted(w, v.relCP.Left.Table)
io.WriteString(w, `, `)
}
}
}
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
}

323
psql/insert_test.go Normal file
View File

@ -0,0 +1,323 @@
package psql
import (
"encoding/json"
"testing"
)
func simpleInsert(t *testing.T) {
gql := `mutation {
user(insert: $data) {
id
}
}`
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func singleInsert(t *testing.T) {
gql := `mutation {
product(id: 15, insert: $insert) {
id
name
}
}`
sql := `WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description", "price", "user_id") SELECT "t"."name", "t"."description", "t"."price", "t"."user_id" FROM "_sg_input" i, 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") 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{
"insert": json.RawMessage(` { "name": "my_name", "price": 6.95, "description": "my_desc", "user_id": 5 }`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "anon")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func bulkInsert(t *testing.T) {
gql := `mutation {
product(name: "test", id: 15, insert: $insert) {
id
name
}
}`
sql := `WITH "_sg_input" AS (SELECT '{{insert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_recordset(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") 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{
"insert": json.RawMessage(` [{ "name": "my_name", "description": "my_desc" }]`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "anon")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func simpleInsertWithPresets(t *testing.T) {
gql := `mutation {
product(insert: $data) {
id
}
}`
sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone, 'now' :: timestamp without time zone, '{{user_id}}' :: bigint FROM "_sg_input" i, 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") 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": "Tomato", "price": 5.76}`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func nestedInsertManyToMany(t *testing.T) {
gql := `mutation {
purchase(insert: $data) {
sale_type
quantity
due_date
customer {
id
full_name
email
}
product {
id
name
price
}
}
}`
sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t RETURNING *), "products" AS (INSERT INTO "products" ("name", "price") SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "product_id", "customer_id") SELECT "t"."sale_type", "t"."quantity", "t"."due_date", "products"."id", "customers"."id" FROM "_sg_input" i, "products", "customers", json_populate_record(NULL::purchases, i.j) t 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), "products" AS (INSERT INTO "products" ("name", "price") SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t RETURNING *), "customers" AS (INSERT INTO "customers" ("full_name", "email") SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t RETURNING *), "purchases" AS (INSERT INTO "purchases" ("sale_type", "quantity", "due_date", "customer_id", "product_id") SELECT "t"."sale_type", "t"."quantity", "t"."due_date", "customers"."id", "products"."id" FROM "_sg_input" i, "customers", "products", json_populate_record(NULL::purchases, i.j) t 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"`
vars := map[string]json.RawMessage{
"data": json.RawMessage(` {
"sale_type": "bought",
"quantity": 5,
"due_date": "now",
"customer": {
"email": "thedude@rug.com",
"full_name": "The Dude"
},
"product": {
"name": "Apple",
"price": 1.25
}
}
`),
}
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 {
t.Fatal(errNotExpected)
}
}
}
func nestedInsertOneToMany(t *testing.T) {
gql := `mutation {
user(insert: $data) {
id
full_name
email
product {
id
name
price
}
}
}`
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 (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->'product') t 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"`
vars := map[string]json.RawMessage{
"data": json.RawMessage(`{
"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"
}
}`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func nestedInsertOneToOne(t *testing.T) {
gql := `mutation {
product(insert: $data) {
id
name
user {
id
full_name
email
}
}
}`
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->'user') t RETURNING *), "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(`{
"name": "Apple",
"price": 1.25,
"created_at": "now",
"updated_at": "now",
"user": {
"hey": {
"now": "what's the matter"
},
"email": "thedude@rug.com",
"full_name": "The Dude",
"created_at": "now",
"updated_at": "now"
}
}`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func nestedInsertOneToManyWithConnect(t *testing.T) {
gql := `mutation {
user(insert: $data) {
id
full_name
email
product {
id
name
price
}
}
}`
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"`
vars := map[string]json.RawMessage{
"data": json.RawMessage(`{
"email": "thedude@rug.com",
"full_name": "The Dude",
"created_at": "now",
"updated_at": "now",
"product": {
"connect": { "id": 5 }
}
}`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func nestedInsertOneToOneWithConnect(t *testing.T) {
gql := `mutation {
product(insert: $data) {
id
name
user {
id
full_name
email
}
}
}`
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"`
vars := map[string]json.RawMessage{
"data": json.RawMessage(`{
"name": "Apple",
"price": 1.25,
"created_at": "now",
"updated_at": "now",
"user": {
"connect": { "id": 5 }
}
}`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func TestCompileInsert(t *testing.T) {
t.Run("simpleInsert", simpleInsert)
t.Run("singleInsert", singleInsert)
t.Run("bulkInsert", bulkInsert)
t.Run("simpleInsertWithPresets", simpleInsertWithPresets)
t.Run("nestedInsertManyToMany", nestedInsertManyToMany)
t.Run("nestedInsertOneToMany", nestedInsertOneToMany)
t.Run("nestedInsertOneToOne", nestedInsertOneToOne)
t.Run("nestedInsertOneToManyWithConnect", nestedInsertOneToManyWithConnect)
t.Run("nestedInsertOneToOneWithConnect", nestedInsertOneToOneWithConnect)
}

View File

@ -2,14 +2,38 @@
package psql package psql
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"github.com/dosco/super-graph/jsn" "github.com/dosco/super-graph/jsn"
"github.com/dosco/super-graph/qcode" "github.com/dosco/super-graph/qcode"
"github.com/dosco/super-graph/util"
) )
type itemType int
const (
itemInsert itemType = iota + 1
itemUpdate
itemConnect
itemDisconnect
itemUnion
)
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} var noLimit = qcode.Paging{NoLimit: true}
func (co *Compiler) compileMutation(qc *qcode.QCode, w io.Writer, vars Variables) (uint32, error) { func (co *Compiler) compileMutation(qc *qcode.QCode, w io.Writer, vars Variables) (uint32, error) {
@ -25,10 +49,6 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w io.Writer, vars Variables
return 0, err return 0, err
} }
io.WriteString(c.w, `WITH `)
quoted(c.w, ti.Name)
io.WriteString(c.w, ` AS `)
switch qc.Type { switch qc.Type {
case qcode.QTInsert: case qcode.QTInsert:
if _, err := c.renderInsert(qc, w, vars, ti); err != nil { if _, err := c.renderInsert(qc, w, vars, ti); err != nil {
@ -54,8 +74,6 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w io.Writer, vars Variables
return 0, errors.New("valid mutations are 'insert', 'update', 'upsert' and 'delete'") return 0, errors.New("valid mutations are 'insert', 'update', 'upsert' and 'delete'")
} }
io.WriteString(c.w, ` RETURNING *) `)
root.Paging = noLimit root.Paging = noLimit
root.DistinctOn = root.DistinctOn[:] root.DistinctOn = root.DistinctOn[:]
root.OrderBy = root.OrderBy[:] root.OrderBy = root.OrderBy[:]
@ -65,54 +83,146 @@ func (co *Compiler) compileMutation(qc *qcode.QCode, w io.Writer, vars Variables
return c.compileQuery(qc, w) return c.compileQuery(qc, w)
} }
func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer, type kvitem struct {
vars Variables, ti *DBTableInfo) (uint32, error) { id int32
_type itemType
insert, ok := vars[qc.ActionVar] key string
if !ok { path []string
return 0, fmt.Errorf("Variable '%s' not defined", qc.ActionVar) val json.RawMessage
ti *DBTableInfo
relCP *DBRel
relPC *DBRel
items []kvitem
} }
jt, array, err := jsn.Tree(insert) type renitem struct {
kvitem
array bool
data map[string]json.RawMessage
}
func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error {
data, array, err := jsn.Tree(item.val)
if err != nil { if err != nil {
return 0, err return err
} }
io.WriteString(c.w, `(WITH "input" AS (SELECT '{{`) var unionize bool
io.WriteString(c.w, qc.ActionVar) id := item.id + 1
io.WriteString(c.w, `}}' :: json AS j) INSERT INTO `)
quoted(c.w, ti.Name)
io.WriteString(c.w, ` (`)
c.renderInsertUpdateColumns(qc, w, jt, ti, false)
io.WriteString(c.w, `)`)
io.WriteString(c.w, ` SELECT `) item.items = make([]kvitem, 0, len(data))
c.renderInsertUpdateColumns(qc, w, jt, ti, true)
io.WriteString(c.w, ` FROM input i, `) for k, v := range data {
if v[0] != '{' && v[0] != '[' {
continue
}
if _, ok := item.ti.ColMap[k]; ok {
continue
}
// Get child-to-parent relationship
relCP, err := c.schema.GetRel(k, item.key)
if err != nil {
var ty itemType
var ok bool
switch item._type {
case itemInsert:
ty, ok = insertTypes[k]
case itemUpdate:
ty, ok = updateTypes[k]
}
if ok {
unionize = true
item1 := item
item1._type = ty
item1.id = id
item1.val = v
item.items = append(item.items, item1)
id++
}
if array {
io.WriteString(c.w, `json_populate_recordset`)
} else { } else {
io.WriteString(c.w, `json_populate_record`) ti, err := c.schema.GetTable(k)
if err != nil {
return err
}
// Get parent-to-child relationship
relPC, err := c.schema.GetRel(item.key, k)
if err != nil {
return err
} }
io.WriteString(c.w, `(NULL::`) item.items = append(item.items, kvitem{
io.WriteString(c.w, ti.Name) id: id,
io.WriteString(c.w, `, i.j) t`) _type: item._type,
key: k,
if w := qc.Selects[0].Where; w != nil && w.Op == qcode.OpFalse { val: v,
io.WriteString(c.w, ` WHERE false`) path: append(item.path, k),
ti: ti,
relCP: relCP,
relPC: relPC,
})
id++
}
} }
return 0, nil if unionize {
item._type = itemUnion
} }
func (c *compilerContext) renderInsertUpdateColumns(qc *qcode.QCode, w io.Writer, // For inserts order the children according to
jt map[string]interface{}, ti *DBTableInfo, values bool) (uint32, error) { // the creation order required by the parent-to-child
// relationships. For example users need to be created
// before the products they own.
// For updates the order defined in the query must be
// the order used.
switch item._type {
case itemInsert:
for _, v := range item.items {
if v.relPC.Type == RelOneToMany {
st.Push(v)
}
}
st.Push(renitem{kvitem: item, array: array, data: data})
for _, v := range item.items {
if 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)
}
st.Push(renitem{kvitem: item, array: array, data: data})
}
return nil
}
func renderInsertUpdateColumns(w io.Writer,
qc *qcode.QCode,
jt map[string]json.RawMessage,
ti *DBTableInfo,
skipcols map[string]struct{},
values bool) (uint32, error) {
root := &qc.Selects[0] root := &qc.Selects[0]
i := 0 n := 0
for _, cn := range ti.Columns { for _, cn := range ti.Columns {
if _, ok := skipcols[cn.Name]; ok {
continue
}
if _, ok := jt[cn.Key]; !ok { if _, ok := jt[cn.Key]; !ok {
continue continue
} }
@ -124,17 +234,16 @@ func (c *compilerContext) renderInsertUpdateColumns(qc *qcode.QCode, w io.Writer
continue continue
} }
} }
if i != 0 { if n != 0 {
io.WriteString(c.w, `, `) io.WriteString(w, `, `)
}
io.WriteString(c.w, `"`)
io.WriteString(c.w, cn.Name)
io.WriteString(c.w, `"`)
i++
} }
if i != 0 && len(root.PresetList) != 0 { if values {
io.WriteString(c.w, `, `) colWithTable(w, "t", cn.Name)
} else {
quoted(w, cn.Name)
}
n++
} }
for i := range root.PresetList { for i := range root.PresetList {
@ -143,83 +252,26 @@ func (c *compilerContext) renderInsertUpdateColumns(qc *qcode.QCode, w io.Writer
if !ok { if !ok {
continue continue
} }
if i != 0 { if _, ok := skipcols[col.Name]; ok {
io.WriteString(c.w, `, `) continue
} }
if i != 0 || n != 0 {
io.WriteString(w, `, `)
}
if values { if values {
io.WriteString(c.w, `'`) io.WriteString(w, `'`)
io.WriteString(c.w, root.PresetMap[cn]) io.WriteString(w, root.PresetMap[cn])
io.WriteString(c.w, `' :: `) io.WriteString(w, `' :: `)
io.WriteString(c.w, col.Type) io.WriteString(w, col.Type)
} else { } else {
io.WriteString(c.w, `"`) quoted(w, cn)
io.WriteString(c.w, cn)
io.WriteString(c.w, `"`)
} }
} }
return 0, nil return 0, nil
} }
func (c *compilerContext) renderUpdate(qc *qcode.QCode, w io.Writer,
vars Variables, ti *DBTableInfo) (uint32, error) {
root := &qc.Selects[0]
update, ok := vars[qc.ActionVar]
if !ok {
return 0, fmt.Errorf("Variable '%s' not defined", qc.ActionVar)
}
jt, array, err := jsn.Tree(update)
if err != nil {
return 0, err
}
io.WriteString(c.w, `(WITH "input" AS (SELECT '{{`)
io.WriteString(c.w, qc.ActionVar)
io.WriteString(c.w, `}}' :: json AS j) UPDATE `)
quoted(c.w, ti.Name)
io.WriteString(c.w, ` SET (`)
c.renderInsertUpdateColumns(qc, w, jt, ti, false)
io.WriteString(c.w, `) = (SELECT `)
c.renderInsertUpdateColumns(qc, w, jt, ti, true)
io.WriteString(c.w, ` FROM input i, `)
if array {
io.WriteString(c.w, `json_populate_recordset`)
} else {
io.WriteString(c.w, `json_populate_record`)
}
io.WriteString(c.w, `(NULL::`)
io.WriteString(c.w, ti.Name)
io.WriteString(c.w, `, i.j) t)`)
io.WriteString(c.w, ` WHERE `)
if err := c.renderWhere(root, ti); err != nil {
return 0, err
}
return 0, nil
}
func (c *compilerContext) renderDelete(qc *qcode.QCode, w io.Writer,
vars Variables, ti *DBTableInfo) (uint32, error) {
root := &qc.Selects[0]
io.WriteString(c.w, `(DELETE FROM `)
quoted(c.w, ti.Name)
io.WriteString(c.w, ` WHERE `)
if err := c.renderWhere(root, ti); err != nil {
return 0, err
}
return 0, nil
}
func (c *compilerContext) renderUpsert(qc *qcode.QCode, w io.Writer, func (c *compilerContext) renderUpsert(qc *qcode.QCode, w io.Writer,
vars Variables, ti *DBTableInfo) (uint32, error) { vars Variables, ti *DBTableInfo) (uint32, error) {
root := &qc.Selects[0] root := &qc.Selects[0]
@ -289,6 +341,8 @@ func (c *compilerContext) renderUpsert(qc *qcode.QCode, w io.Writer,
i++ i++
} }
io.WriteString(c.w, ` RETURNING *) `)
return 0, nil return 0, nil
} }
@ -297,3 +351,153 @@ func quoted(w io.Writer, identifier string) {
io.WriteString(w, identifier) io.WriteString(w, identifier)
io.WriteString(w, `"`) io.WriteString(w, `"`)
} }
func joinPath(w io.Writer, path []string) {
for i := range path {
if i != 0 {
io.WriteString(w, `->`)
}
io.WriteString(w, `'`)
io.WriteString(w, path[i])
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

@ -5,77 +5,6 @@ import (
"testing" "testing"
) )
func simpleInsert(t *testing.T) {
gql := `mutation {
user(insert: $data) {
id
}
}`
sql := `WITH "users" AS (WITH "input" AS (SELECT '{{data}}' :: json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func singleInsert(t *testing.T) {
gql := `mutation {
product(id: 15, insert: $insert) {
id
name
}
}`
sql := `WITH "products" AS (WITH "input" AS (SELECT '{{insert}}' :: json AS j) INSERT INTO "products" ("name", "description", "user_id") SELECT "name", "description", "user_id" FROM input i, 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") 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{
"insert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc", "user_id": 5 }`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "anon")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func bulkInsert(t *testing.T) {
gql := `mutation {
product(name: "test", id: 15, insert: $insert) {
id
name
}
}`
sql := `WITH "products" AS (WITH "input" AS (SELECT '{{insert}}' :: json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_recordset(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', 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{
"insert": json.RawMessage(` [{ "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }]`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "anon")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func singleUpsert(t *testing.T) { func singleUpsert(t *testing.T) {
gql := `mutation { gql := `mutation {
product(upsert: $upsert) { product(upsert: $upsert) {
@ -84,10 +13,10 @@ func singleUpsert(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (WITH "input" AS (SELECT '{{upsert}}' :: json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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 '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"upsert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), "upsert": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "user") resSQL, err := compileGQLToPSQL(gql, vars, "user")
@ -108,10 +37,10 @@ func singleUpsertWhere(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (WITH "input" AS (SELECT '{{upsert}}' :: json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t ON CONFLICT (id) WHERE (("products"."price") > 3) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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 '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) WHERE (("products"."price") > 3) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"upsert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), "upsert": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "user") resSQL, err := compileGQLToPSQL(gql, vars, "user")
@ -132,10 +61,10 @@ func bulkUpsert(t *testing.T) {
} }
}` }`
sql := `WITH "products" AS (WITH "input" AS (SELECT '{{upsert}}' :: json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_recordset(NULL::products, i.j) t ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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 '{{upsert}}' :: json AS j), "products" AS (INSERT INTO "products" ("name", "description") SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_recordset(NULL::products, i.j) t RETURNING *) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description 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"`
vars := map[string]json.RawMessage{ vars := map[string]json.RawMessage{
"upsert": json.RawMessage(` [{ "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }]`), "upsert": json.RawMessage(` [{ "name": "my_name", "description": "my_desc" }]`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "user") resSQL, err := compileGQLToPSQL(gql, vars, "user")
@ -148,30 +77,6 @@ func bulkUpsert(t *testing.T) {
} }
} }
func singleUpdate(t *testing.T) {
gql := `mutation {
product(id: 15, update: $update, where: { id: { eq: 1 } }) {
id
name
}
}`
sql := `WITH "products" AS (WITH "input" AS (SELECT '{{update}}' :: json AS j) UPDATE "products" SET ("name", "description") = (SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."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"`
vars := map[string]json.RawMessage{
"update": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "anon")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func delete(t *testing.T) { func delete(t *testing.T) {
gql := `mutation { gql := `mutation {
product(delete: true, where: { id: { eq: 1 } }) { product(delete: true, where: { id: { eq: 1 } }) {
@ -183,7 +88,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 *) 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{ vars := map[string]json.RawMessage{
"update": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), "update": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`),
} }
resSQL, err := compileGQLToPSQL(gql, vars, "user") resSQL, err := compileGQLToPSQL(gql, vars, "user")
@ -196,111 +101,59 @@ func delete(t *testing.T) {
} }
} }
func blockedInsert(t *testing.T) { // func blockedInsert(t *testing.T) {
gql := `mutation { // gql := `mutation {
user(insert: $data) { // user(insert: $data) {
id // id
} // }
}` // }`
sql := `WITH "users" AS (WITH "input" AS (SELECT '{{data}}' :: json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t WHERE false RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"` // sql := `WITH "users" AS (WITH "input" AS (SELECT '{{data}}' :: json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t WHERE false RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ // vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), // "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`),
} // }
resSQL, err := compileGQLToPSQL(gql, vars, "bad_dude") // resSQL, err := compileGQLToPSQL(gql, vars, "bad_dude")
if err != nil { // if err != nil {
t.Fatal(err) // t.Fatal(err)
} // }
// fmt.Println(string(resSQL))
if string(resSQL) != sql { // if string(resSQL) != sql {
t.Fatal(errNotExpected) // t.Fatal(errNotExpected)
} // }
} // }
func blockedUpdate(t *testing.T) { // func blockedUpdate(t *testing.T) {
gql := `mutation { // gql := `mutation {
user(where: { id: { lt: 5 } }, update: $data) { // user(where: { id: { lt: 5 } }, update: $data) {
id // id
email // email
} // }
}` // }`
sql := `WITH "users" AS (WITH "input" AS (SELECT '{{data}}' :: json AS j) UPDATE "users" SET ("full_name", "email") = (SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t) WHERE false 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"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"` // sql := `WITH "users" AS (WITH "input" AS (SELECT '{{data}}' :: json AS j) UPDATE "users" SET ("full_name", "email") = (SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t) WHERE false 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"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"`
vars := map[string]json.RawMessage{ // vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), // "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`),
} // }
resSQL, err := compileGQLToPSQL(gql, vars, "bad_dude") // resSQL, err := compileGQLToPSQL(gql, vars, "bad_dude")
if err != nil { // if err != nil {
t.Fatal(err) // t.Fatal(err)
} // }
if string(resSQL) != sql { // if string(resSQL) != sql {
t.Fatal(errNotExpected) // t.Fatal(errNotExpected)
} // }
} // }
func simpleInsertWithPresets(t *testing.T) {
gql := `mutation {
product(insert: $data) {
id
}
}`
sql := `WITH "products" AS (WITH "input" AS (SELECT '{{data}}' :: json AS j) INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "name", "price", 'now' :: timestamp without time zone, 'now' :: timestamp without time zone, '{{user_id}}' :: bigint FROM input i, 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") 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": "Tomato", "price": 5.76}`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func simpleUpdateWithPresets(t *testing.T) {
gql := `mutation {
product(update: $data) {
id
}
}`
sql := `WITH "products" AS (WITH "input" AS (SELECT '{{data}}' :: json AS j) UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "name", "price", 'now' :: timestamp without time zone FROM 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"`
vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"name": "Apple", "price": 1.25}`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func TestCompileMutate(t *testing.T) { func TestCompileMutate(t *testing.T) {
t.Run("simpleInsert", simpleInsert)
t.Run("singleInsert", singleInsert)
t.Run("bulkInsert", bulkInsert)
t.Run("singleUpdate", singleUpdate)
t.Run("singleUpsert", singleUpsert) t.Run("singleUpsert", singleUpsert)
t.Run("singleUpsertWhere", singleUpsertWhere) t.Run("singleUpsertWhere", singleUpsertWhere)
t.Run("bulkUpsert", bulkUpsert) t.Run("bulkUpsert", bulkUpsert)
t.Run("delete", delete) t.Run("delete", delete)
t.Run("blockedInsert", blockedInsert) // t.Run("blockedInsert", blockedInsert)
t.Run("blockedUpdate", blockedUpdate) // t.Run("blockedUpdate", blockedUpdate)
t.Run("simpleInsertWithPresets", simpleInsertWithPresets)
t.Run("simpleUpdateWithPresets", simpleUpdateWithPresets)
} }

View File

@ -138,7 +138,7 @@ func TestMain(m *testing.M) {
columns := [][]DBColumn{ columns := [][]DBColumn{
[]DBColumn{ []DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: false}, DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 4, Name: "email", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 4, Name: "email", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
@ -149,7 +149,7 @@ func TestMain(m *testing.M) {
DBColumn{ID: 9, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 9, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 10, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false}}, DBColumn{ID: 10, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false}},
[]DBColumn{ []DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: false}, DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 4, Name: "avatar", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 4, Name: "avatar", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
@ -161,7 +161,7 @@ func TestMain(m *testing.M) {
DBColumn{ID: 10, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 10, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 11, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false}}, DBColumn{ID: 11, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false}},
[]DBColumn{ []DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: false}, DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "name", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 2, Name: "name", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 3, Name: "description", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 3, Name: "description", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 4, Name: "price", Type: "numeric(7,2)", NotNull: false, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 4, Name: "price", Type: "numeric(7,2)", NotNull: false, PrimaryKey: false, UniqueKey: false},
@ -171,7 +171,7 @@ func TestMain(m *testing.M) {
DBColumn{ID: 8, Name: "tsv", Type: "tsvector", NotNull: false, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 8, Name: "tsv", Type: "tsvector", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 9, Name: "tags", Type: "text[]", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "tags", FKeyColID: []int16{3}, Array: true}}, DBColumn{ID: 9, Name: "tags", Type: "text[]", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "tags", FKeyColID: []int16{3}, Array: true}},
[]DBColumn{ []DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: false}, DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "customer_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "customers", FKeyColID: []int16{1}}, DBColumn{ID: 2, Name: "customer_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "customers", FKeyColID: []int16{1}},
DBColumn{ID: 3, Name: "product_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "products", FKeyColID: []int16{1}}, DBColumn{ID: 3, Name: "product_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "products", FKeyColID: []int16{1}},
DBColumn{ID: 4, Name: "sale_type", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 4, Name: "sale_type", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false},
@ -179,7 +179,7 @@ func TestMain(m *testing.M) {
DBColumn{ID: 6, Name: "due_date", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 6, Name: "due_date", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 7, Name: "returned", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false}}, DBColumn{ID: 7, Name: "returned", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false}},
[]DBColumn{ []DBColumn{
DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: false}, DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: true},
DBColumn{ID: 2, Name: "name", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false}, DBColumn{ID: 2, Name: "name", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false},
DBColumn{ID: 3, Name: "slug", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false}}, DBColumn{ID: 3, Name: "slug", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false}},
} }

View File

@ -81,7 +81,7 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) {
c := &compilerContext{w, qc.Selects, co} c := &compilerContext{w, qc.Selects, co}
multiRoot := (len(qc.Roots) > 1) multiRoot := (len(qc.Roots) > 1)
st := NewStack() st := NewIntStack()
if multiRoot { if multiRoot {
io.WriteString(c.w, `SELECT row_to_json("json_root") FROM (SELECT `) io.WriteString(c.w, `SELECT row_to_json("json_root") FROM (SELECT `)
@ -227,7 +227,7 @@ func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (u
} }
switch rel.Type { switch rel.Type {
case RelOneToMany: case RelOneToOne, RelOneToMany:
if _, ok := colmap[rel.Right.Col]; !ok { if _, ok := colmap[rel.Right.Col]; !ok {
cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Right.Col, FieldName: rel.Right.Col}) cols = append(cols, &qcode.Column{Table: ti.Name, Name: rel.Right.Col, FieldName: rel.Right.Col})
} }
@ -759,7 +759,7 @@ func (c *compilerContext) renderRelationshipByName(table, parent string, id int3
io.WriteString(c.w, `((`) io.WriteString(c.w, `((`)
switch rel.Type { switch rel.Type {
case RelOneToMany: case RelOneToOne, RelOneToMany:
//fmt.Fprintf(w, `(("%s"."%s") = ("%s_%d"."%s"))`, //fmt.Fprintf(w, `(("%s"."%s") = ("%s_%d"."%s"))`,
//c.sel.Name, rel.Left.Col, c.parent.Name, c.parent.ID, rel.Right.Col) //c.sel.Name, rel.Left.Col, c.parent.Name, c.parent.ID, rel.Right.Col)

View File

@ -27,7 +27,8 @@ type DBTableInfo struct {
type RelType int type RelType int
const ( const (
RelOneToMany RelType = iota + 1 RelOneToOne RelType = iota + 1
RelOneToMany
RelOneToManyThrough RelOneToManyThrough
RelRemote RelRemote
) )
@ -37,10 +38,12 @@ type DBRel struct {
Through string Through string
ColT string ColT string
Left struct { Left struct {
Table string
Col string Col string
Array bool Array bool
} }
Right struct { Right struct {
Table string
Col string Col string
Array bool Array bool
} }
@ -153,11 +156,21 @@ func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error {
fcid, ti.Name) fcid, ti.Name)
} }
var rel1, rel2 *DBRel
// One-to-many relation between current table and the // One-to-many relation between current table and the
// table in the foreign key // table in the foreign key
rel1 := &DBRel{Type: RelOneToMany} if fc.UniqueKey {
rel1 = &DBRel{Type: RelOneToOne}
} else {
rel1 = &DBRel{Type: RelOneToMany}
}
rel1.Left.Table = t.Name
rel1.Left.Col = c.Name rel1.Left.Col = c.Name
rel1.Left.Array = c.Array rel1.Left.Array = c.Array
rel1.Right.Table = c.FKeyTable
rel1.Right.Col = fc.Name rel1.Right.Col = fc.Name
rel1.Right.Array = fc.Array rel1.Right.Array = fc.Array
@ -167,9 +180,17 @@ func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error {
// One-to-many reverse relation between the foreign key table and the // One-to-many reverse relation between the foreign key table and the
// the current table // the current table
rel2 := &DBRel{Type: RelOneToMany} if c.UniqueKey {
rel2 = &DBRel{Type: RelOneToOne}
} else {
rel2 = &DBRel{Type: RelOneToMany}
}
rel2.Left.Table = c.FKeyTable
rel2.Left.Col = fc.Name rel2.Left.Col = fc.Name
rel2.Left.Array = fc.Array rel2.Left.Array = fc.Array
rel2.Right.Table = t.Name
rel2.Right.Col = c.Name rel2.Right.Col = c.Name
rel2.Right.Array = c.Array rel2.Right.Array = c.Array
@ -225,8 +246,13 @@ func (s *DBSchema) updateSchemaOTMT(
rel1 := &DBRel{Type: RelOneToManyThrough} rel1 := &DBRel{Type: RelOneToManyThrough}
rel1.Through = ti.Name rel1.Through = ti.Name
rel1.ColT = col2.Name rel1.ColT = col2.Name
rel1.Left.Table = col2.FKeyTable
rel1.Left.Col = fc2.Name rel1.Left.Col = fc2.Name
rel1.Right.Table = ti.Name
rel1.Right.Col = col1.Name rel1.Right.Col = col1.Name
if err := s.SetRel(t1, t2, rel1); err != nil { if err := s.SetRel(t1, t2, rel1); err != nil {
return err return err
} }
@ -236,8 +262,13 @@ func (s *DBSchema) updateSchemaOTMT(
rel2 := &DBRel{Type: RelOneToManyThrough} rel2 := &DBRel{Type: RelOneToManyThrough}
rel2.Through = ti.Name rel2.Through = ti.Name
rel2.ColT = col1.Name rel2.ColT = col1.Name
rel2.Left.Table = col1.FKeyTable
rel2.Left.Col = fc1.Name rel2.Left.Col = fc1.Name
rel2.Right.Table = ti.Name
rel2.Right.Col = col2.Name rel2.Right.Col = col2.Name
if err := s.SetRel(t2, t1, rel2); err != nil { if err := s.SetRel(t2, t1, rel2); err != nil {
return err return err
} }

View File

@ -1,47 +0,0 @@
package psql
type Stack struct {
stA [20]int32
st []int32
top int
}
// Create a new Stack
func NewStack() *Stack {
s := &Stack{top: -1}
s.st = s.stA[:0]
return s
}
// Return the number of items in the Stack
func (s *Stack) Len() int {
return (s.top + 1)
}
// View the top item on the Stack
func (s *Stack) Peek() int32 {
if s.top == -1 {
return -1
}
return s.st[s.top]
}
// Pop the top item of the Stack and return it
func (s *Stack) Pop() int32 {
if s.top == -1 {
return -1
}
s.top--
return s.st[(s.top + 1)]
}
// Push a value onto the top of the Stack
func (s *Stack) Push(value int32) {
s.top++
if len(s.st) <= s.top {
s.st = append(s.st, value)
} else {
s.st[s.top] = value
}
}

47
psql/stack_int.go Normal file
View File

@ -0,0 +1,47 @@
package psql
type IntStack struct {
stA [20]int32
st []int32
top int
}
// Create a new IntStack
func NewIntStack() *IntStack {
s := &IntStack{top: -1}
s.st = s.stA[:0]
return s
}
// Return the number of items in the IntStack
func (s *IntStack) Len() int {
return (s.top + 1)
}
// View the top item on the IntStack
func (s *IntStack) Peek() int32 {
if s.top == -1 {
return -1
}
return s.st[s.top]
}
// Pop the top item of the IntStack and return it
func (s *IntStack) Pop() int32 {
if s.top == -1 {
return -1
}
s.top--
return s.st[(s.top + 1)]
}
// Push a value onto the top of the IntStack
func (s *IntStack) Push(value int32) {
s.top++
if len(s.st) <= s.top {
s.st = append(s.st, value)
} else {
s.st[s.top] = value
}
}

22
psql/strings.go Normal file
View File

@ -0,0 +1,22 @@
package psql
import "fmt"
func (rt RelType) String() string {
switch rt {
case RelOneToOne:
return "one to one"
case RelOneToMany:
return "one to many"
case RelOneToManyThrough:
return "one to many through"
case RelRemote:
return "remote"
}
return ""
}
func (re *DBRel) String() string {
return fmt.Sprintf("'%s.%s' --(%s)--> '%s.%s'",
re.Left.Table, re.Left.Col, re.Type, re.Right.Table, re.Right.Col)
}

View File

@ -106,8 +106,10 @@ AND pg_catalog.pg_table_is_visible(c.oid);`
return nil, err return nil, err
} }
t.Key = strings.ToLower(t.Name) t.Key = strings.ToLower(t.Name)
if t.Key != "schema_migrations" && t.Key != "ar_internal_metadata" {
tables = append(tables, t) tables = append(tables, t)
} }
}
return tables, nil return tables, nil
} }
@ -185,6 +187,7 @@ ORDER BY id;`
if v, ok := cmap[c.ID]; ok { if v, ok := cmap[c.ID]; ok {
if c.PrimaryKey { if c.PrimaryKey {
v.PrimaryKey = true v.PrimaryKey = true
v.UniqueKey = true
} }
if c.NotNull { if c.NotNull {
v.NotNull = true v.NotNull = true
@ -212,6 +215,9 @@ ORDER BY id;`
return nil, err return nil, err
} }
c.Key = strings.ToLower(c.Name) c.Key = strings.ToLower(c.Name)
if c.PrimaryKey {
c.UniqueKey = true
}
cmap[c.ID] = c cmap[c.ID] = c
} }
} }

179
psql/update.go Normal file
View File

@ -0,0 +1,179 @@
package psql
import (
"fmt"
"io"
"github.com/dosco/super-graph/qcode"
"github.com/dosco/super-graph/util"
)
func (c *compilerContext) renderUpdate(qc *qcode.QCode, w io.Writer,
vars Variables, ti *DBTableInfo) (uint32, error) {
insert, ok := vars[qc.ActionVar]
if !ok {
return 0, fmt.Errorf("Variable '%s' not !defined", qc.ActionVar)
}
io.WriteString(c.w, `WITH "_sg_input" AS (SELECT '{{`)
io.WriteString(c.w, qc.ActionVar)
io.WriteString(c.w, `}}' :: json AS j)`)
st := util.NewStack()
st.Push(kvitem{_type: itemUpdate, key: ti.Name, val: insert, ti: ti})
for {
if st.Len() == 0 {
break
}
intf := st.Pop()
switch item := intf.(type) {
case kvitem:
if err := c.handleKVItem(st, item); err != nil {
return 0, err
}
case renitem:
var err error
// if w := qc.Selects[0].Where; w != nil && w.Op == qcode.OpFalse {
// io.WriteString(c.w, ` WHERE false`)
// }
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 itemUnion:
err = c.renderUpdateUnionStmt(w, item)
}
if err != nil {
return 0, err
}
}
}
io.WriteString(c.w, ` `)
return 0, nil
}
func (c *compilerContext) renderUpdateStmt(w io.Writer, qc *qcode.QCode, item renitem) error {
ti := item.ti
jt := item.data
io.WriteString(c.w, `, `)
renderCteName(c.w, item.kvitem)
io.WriteString(c.w, ` AS (`)
io.WriteString(w, `UPDATE `)
quoted(w, ti.Name)
io.WriteString(w, ` SET (`)
renderInsertUpdateColumns(w, qc, jt, ti, nil, false)
io.WriteString(w, `) = (SELECT `)
renderInsertUpdateColumns(w, qc, jt, ti, nil, true)
io.WriteString(w, ` FROM "_sg_input" i, `)
if item.array {
io.WriteString(w, `json_populate_recordset`)
} else {
io.WriteString(w, `json_populate_record`)
}
io.WriteString(w, `(NULL::`)
io.WriteString(w, ti.Name)
io.WriteString(w, `, i.j) t`)
io.WriteString(w, ` WHERE `)
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, `((`)
colWithTable(w, rel.Left.Table, rel.Left.Col)
io.WriteString(w, `) = (`)
colWithTable(w, rel.Right.Table, rel.Right.Col)
io.WriteString(w, `)`)
if item.relPC.Type == RelOneToMany {
if conn, ok := item.data["where"]; ok {
io.WriteString(w, ` AND `)
renderWhereFromJSON(w, conn)
} else if conn, ok := item.data["_where"]; ok {
io.WriteString(w, ` AND `)
renderWhereFromJSON(w, conn)
}
}
io.WriteString(w, `)`)
} else {
if err := c.renderWhere(&qc.Selects[0], ti); err != nil {
return err
}
}
io.WriteString(w, `) RETURNING *)`)
return nil
}
func (c *compilerContext) renderUpdateUnionStmt(w io.Writer, item renitem) error {
renderCteName(w, item.kvitem)
io.WriteString(w, ` AS (`)
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++
}
}
io.WriteString(w, `)`)
return nil
}
func (c *compilerContext) renderDelete(qc *qcode.QCode, w io.Writer,
vars Variables, ti *DBTableInfo) (uint32, error) {
root := &qc.Selects[0]
io.WriteString(c.w, `WITH `)
quoted(c.w, ti.Name)
io.WriteString(c.w, ` AS (DELETE FROM `)
quoted(c.w, ti.Name)
io.WriteString(c.w, ` WHERE `)
if err := c.renderWhere(root, ti); err != nil {
return 0, err
}
io.WriteString(c.w, ` RETURNING *) `)
return 0, nil
}

279
psql/update_test.go Normal file
View File

@ -0,0 +1,279 @@
package psql
import (
"encoding/json"
"fmt"
"testing"
)
func singleUpdate(t *testing.T) {
gql := `mutation {
product(id: 15, update: $update, where: { id: { eq: 1 } }) {
id
name
}
}`
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"`
vars := map[string]json.RawMessage{
"update": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "anon")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func simpleUpdateWithPresets(t *testing.T) {
gql := `mutation {
product(update: $data) {
id
}
}`
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"`
vars := map[string]json.RawMessage{
"data": json.RawMessage(`{"name": "Apple", "price": 1.25}`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "user")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func nestedUpdateManyToMany(t *testing.T) {
gql := `mutation {
purchase(update: $data, id: 5) {
sale_type
quantity
due_date
customer {
id
full_name
email
}
product {
id
name
price
}
}
}`
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"`
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"`
vars := map[string]json.RawMessage{
"data": json.RawMessage(` {
"sale_type": "bought",
"quantity": 5,
"due_date": "now",
"customer": {
"email": "thedude@rug.com",
"full_name": "The Dude"
},
"product": {
"name": "Apple",
"price": 1.25
}
}
`),
}
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 {
t.Fatal(errNotExpected)
}
}
}
func nestedUpdateOneToMany(t *testing.T) {
gql := `mutation {
user(update: $data, where: { id: { eq: 8 } }) {
id
full_name
email
product {
id
name
price
}
}
}`
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"`
vars := map[string]json.RawMessage{
"data": json.RawMessage(`{
"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"
}
}`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func nestedUpdateOneToOne(t *testing.T) {
gql := `mutation {
product(update: $data, id: 6) {
id
name
user {
id
full_name
email
}
}
}`
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"`
vars := map[string]json.RawMessage{
"data": json.RawMessage(`{
"name": "Apple",
"price": 1.25,
"created_at": "now",
"updated_at": "now",
"user": {
"email": "thedude@rug.com"
}
}`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func nestedUpdateOneToManyWithConnect(t *testing.T) {
gql := `mutation {
user(update: $data, id: 6) {
id
full_name
email
product {
id
name
price
}
}
}`
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"`
vars := map[string]json.RawMessage{
"data": json.RawMessage(`{
"email": "thedude@rug.com",
"full_name": "The Dude",
"created_at": "now",
"updated_at": "now",
"product": {
"connect": { "id": 7 },
"disconnect": { "id": 8 }
}
}`),
}
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 {
t.Fatal(errNotExpected)
}
}
}
func nestedUpdateOneToOneWithConnect(t *testing.T) {
gql := `mutation {
product(update: $data, id: 9) {
id
name
user {
id
full_name
email
}
}
}`
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"`
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" }
}
}`),
}
resSQL, err := compileGQLToPSQL(gql, vars, "admin")
if err != nil {
t.Fatal(err)
}
fmt.Println(string(resSQL))
if string(resSQL) != sql {
t.Fatal(errNotExpected)
}
}
func TestCompileUpdate(t *testing.T) {
t.Run("singleUpdate", singleUpdate)
t.Run("simpleUpdateWithPresets", simpleUpdateWithPresets)
t.Run("nestedUpdateManyToMany", nestedUpdateManyToMany)
t.Run("nestedUpdateOneToMany", nestedUpdateOneToMany)
t.Run("nestedUpdateOneToOne", nestedUpdateOneToOne)
t.Run("nestedUpdateOneToManyWithConnect", nestedUpdateOneToManyWithConnect)
t.Run("nestedUpdateOneToOneWithConnect", nestedUpdateOneToOneWithConnect)
}

View File

@ -28,7 +28,7 @@ type Pos int
// item represents a token or text string returned from the scanner. // item represents a token or text string returned from the scanner.
type item struct { type item struct {
typ itemType // The type of this item. _type itemType // The type of this item.
pos Pos // The starting position, in bytes, of this item in the input string. pos Pos // The starting position, in bytes, of this item in the input string.
end Pos // The ending position, in bytes, of this item in the input string. end Pos // The ending position, in bytes, of this item in the input string.
line uint16 // The line number at the start of this item. line uint16 // The line number at the start of this item.
@ -211,7 +211,7 @@ func lex(l *lexer, input []byte) error {
l.run() l.run()
if last := l.items[len(l.items)-1]; last.typ == itemError { if last := l.items[len(l.items)-1]; last._type== itemError {
return l.err return l.err
} }
return nil return nil
@ -435,7 +435,7 @@ func lowercase(b []byte, s Pos, e Pos) {
func (i *item) String() string { func (i *item) String() string {
var v string var v string
switch i.typ { switch i._type{
case itemEOF: case itemEOF:
v = "EOF" v = "EOF"
case itemError: case itemError:

View File

@ -169,7 +169,7 @@ func (p *Parser) next() item {
n := p.pos + 1 n := p.pos + 1
if n >= len(p.items) { if n >= len(p.items) {
p.err = errEOT p.err = errEOT
return item{typ: itemEOF} return item{_type: itemEOF}
} }
p.pos = n p.pos = n
return p.items[p.pos] return p.items[p.pos]
@ -186,14 +186,14 @@ func (p *Parser) ignore() {
func (p *Parser) peek(types ...itemType) bool { func (p *Parser) peek(types ...itemType) bool {
n := p.pos + 1 n := p.pos + 1
if p.items[n].typ == itemEOF { if p.items[n]._type == itemEOF {
return false return false
} }
if n >= len(p.items) { if n >= len(p.items) {
return false return false
} }
for i := 0; i < len(types); i++ { for i := 0; i < len(types); i++ {
if p.items[n].typ == types[i] { if p.items[n]._type == types[i] {
return true return true
} }
} }
@ -210,7 +210,7 @@ func (p *Parser) parseOp() (*Operation, error) {
op := opPool.Get().(*Operation) op := opPool.Get().(*Operation)
op.Reset() op.Reset()
switch item.typ { switch item._type {
case itemQuery: case itemQuery:
op.Type = opQuery op.Type = opQuery
case itemMutation: case itemMutation:
@ -471,7 +471,7 @@ func (p *Parser) parseValue() (*Node, error) {
node := nodePool.Get().(*Node) node := nodePool.Get().(*Node)
node.Reset() node.Reset()
switch item.typ { switch item._type {
case itemIntVal: case itemIntVal:
node.Type = NodeInt node.Type = NodeInt
case itemFloatVal: case itemFloatVal:

View File

@ -1086,7 +1086,6 @@ func (t ExpOp) String() string {
} }
func FreeExp(ex *Exp) { func FreeExp(ex *Exp) {
// fmt.Println(">", ex.doFree)
if ex.doFree { if ex.doFree {
expPool.Put(ex) expPool.Put(ex)
} }

View File

@ -3,6 +3,7 @@ package serv
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -48,7 +49,7 @@ func argMap(ctx context.Context, vars []byte) func(w io.Writer, tag string) (int
func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) { func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
vars := make([]interface{}, len(args)) vars := make([]interface{}, len(args))
var fields map[string]interface{} var fields map[string]json.RawMessage
var err error var err error
if len(ctx.req.Vars) != 0 { if len(ctx.req.Vars) != 0 {
@ -86,10 +87,19 @@ func argList(ctx *coreContext, args [][]byte) ([]interface{}, error) {
default: default:
if v, ok := fields[string(av)]; ok { if v, ok := fields[string(av)]; ok {
switch v[0] {
case '[', '{':
vars[i] = v vars[i] = v
default:
var val interface{}
if err := json.Unmarshal(v, &val); err != nil {
return nil, err
}
vars[i] = val
}
} else { } else {
return nil, fmt.Errorf("query requires variable $%s", string(av)) return nil, fmt.Errorf("query requires variable $%s", string(av))
} }
} }
} }

View File

@ -260,8 +260,14 @@ func (c *coreContext) resolveSQL() ([]byte, *stmt, error) {
} }
func (c *coreContext) executeRoleQuery(tx pgx.Tx) (string, error) { func (c *coreContext) executeRoleQuery(tx pgx.Tx) (string, error) {
userID := c.Value(userIDKey)
if userID == nil {
return "anon", nil
}
var role string var role string
row := tx.QueryRow(c.Context, "_sg_get_role", c.req.role, 1) row := tx.QueryRow(c.Context, "_sg_get_role", userID, c.req.role)
if err := row.Scan(&role); err != nil { if err := row.Scan(&role); err != nil {
return "", err return "", err

View File

@ -15,7 +15,9 @@ func health(w http.ResponseWriter, _ *http.Request) {
return return
} }
ctx, _ := context.WithTimeout(context.Background(), conf.DB.PingTimeout) ctx, cancel := context.WithTimeout(context.Background(), conf.DB.PingTimeout)
defer cancel()
if err := conn.Conn().Ping(ctx); err != nil { if err := conn.Conn().Ping(ctx); err != nil {
errlog.Error().Err(err).Msg("error pinging database") errlog.Error().Err(err).Msg("error pinging database")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)

View File

@ -70,6 +70,12 @@ func prepareStmt(gql string, vars []byte) error {
qt := qcode.GetQType(gql) qt := qcode.GetQType(gql)
q := []byte(gql) q := []byte(gql)
if len(vars) == 0 {
logger.Debug().Msgf("Prepared statement:\n%s\n", gql)
} else {
logger.Debug().Msgf("Prepared statement:\n%s\n%s\n", vars, gql)
}
tx, err := db.Begin(context.Background()) tx, err := db.Begin(context.Background())
if err != nil { if err != nil {
return err return err
@ -91,12 +97,16 @@ func prepareStmt(gql string, vars []byte) error {
return err return err
} }
logger.Debug().Msg("Prepared statement role: user")
err = prepare(tx, stmts1, gqlHash(gql, vars, "user")) err = prepare(tx, stmts1, gqlHash(gql, vars, "user"))
if err != nil { if err != nil {
return err return err
} }
if conf.isAnonRoleDefined() { if conf.isAnonRoleDefined() {
logger.Debug().Msg("Prepared statement for role: anon")
stmts2, err := buildRoleStmt(q, vars, "anon") stmts2, err := buildRoleStmt(q, vars, "anon")
if err != nil { if err != nil {
return err return err
@ -110,6 +120,8 @@ func prepareStmt(gql string, vars []byte) error {
case qcode.QTMutation: case qcode.QTMutation:
for _, role := range conf.Roles { for _, role := range conf.Roles {
logger.Debug().Msgf("Prepared statement for role: %s", role.Name)
stmts, err := buildRoleStmt(q, vars, role.Name) stmts, err := buildRoleStmt(q, vars, role.Name)
if err != nil { if err != nil {
return err return err
@ -122,12 +134,6 @@ func prepareStmt(gql string, vars []byte) error {
} }
} }
if len(vars) == 0 {
logger.Debug().Msgf("Building prepared statement for:\n %s", gql)
} else {
logger.Debug().Msgf("Building prepared statement:\n %s\n%s", vars, gql)
}
if err := tx.Commit(context.Background()); err != nil { if err := tx.Commit(context.Background()); err != nil {
return err return err
} }
@ -160,7 +166,11 @@ func prepareRoleStmt(tx pgx.Tx) error {
w := &bytes.Buffer{} w := &bytes.Buffer{}
io.WriteString(w, `SELECT (CASE`) io.WriteString(w, `SELECT (CASE WHEN EXISTS (`)
io.WriteString(w, conf.RolesQuery)
io.WriteString(w, `) THEN `)
io.WriteString(w, `(SELECT (CASE`)
for _, role := range conf.Roles { for _, role := range conf.Roles {
if len(role.Match) == 0 { if len(role.Match) == 0 {
continue continue
@ -174,7 +184,8 @@ func prepareRoleStmt(tx pgx.Tx) error {
io.WriteString(w, ` ELSE {{role}} END) FROM (`) io.WriteString(w, ` ELSE {{role}} END) FROM (`)
io.WriteString(w, conf.RolesQuery) io.WriteString(w, conf.RolesQuery)
io.WriteString(w, `) AS "_sg_auth_roles_query"`) io.WriteString(w, `) AS "_sg_auth_roles_query" LIMIT 1) `)
io.WriteString(w, `ELSE 'anon' END) FROM (VALUES (1)) AS "_sg_auth_filler" LIMIT 1; `)
roleSQL, _ := processTemplate(w.String()) roleSQL, _ := processTemplate(w.String())