Add nested mutations
This commit is contained in:
parent
96ed3413fc
commit
6831d3f56f
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,6 @@ services:
|
||||||
command: wtc
|
command: wtc
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
- rails_app
|
# - rails_app
|
||||||
|
|
||||||
# - redis
|
# - redis
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
432
psql/mutate.go
432
psql/mutate.go
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
14
serv/args.go
14
serv/args.go
|
@ -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))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue