From 52f3b1c7a22bf324da6a801c29f909f8dc439721 Mon Sep 17 00:00:00 2001 From: Vikram Rangnekar Date: Sun, 26 Jan 2020 01:10:54 -0500 Subject: [PATCH] Add mutation support for connect / disconnect with array relationships --- psql/insert_test.go | 47 ++++++++++++++- psql/mutate.go | 144 +++++++++++++++++++++++++++++++++----------- psql/schema.go | 10 ++- psql/update.go | 14 ++++- psql/update_test.go | 40 ++++++++++-- qcode/parse.go | 5 -- serv/cmd_serv.go | 12 ++-- 7 files changed, 218 insertions(+), 54 deletions(-) diff --git a/psql/insert_test.go b/psql/insert_test.go index e607930..0abc28b 100644 --- a/psql/insert_test.go +++ b/psql/insert_test.go @@ -249,7 +249,7 @@ func nestedInsertOneToManyWithConnect(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *), "products" AS ( UPDATE "products" SET "user_id" = "users"."id"FROM "users" WHERE ("products"."id" = '5') RETURNING "products".*) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (INSERT INTO "users" ("full_name", "email", "created_at", "updated_at") SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t RETURNING *), "products" AS ( UPDATE "products" SET "user_id" = "users"."id" FROM "users" WHERE ("products"."id"= ((i.j->'product'->'connect'->>'id'))::bigint) RETURNING "products".*) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{ @@ -278,6 +278,10 @@ func nestedInsertOneToOneWithConnect(t *testing.T) { product(insert: $data) { id name + tags { + id + name + } user { id full_name @@ -286,7 +290,7 @@ func nestedInsertOneToOneWithConnect(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM "users" WHERE "users"."id" = '5' LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user", "tags_2_join"."json_2" AS "tags") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_2"), '[]') AS "json_2" FROM (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "tags_2"."id" AS "id", "tags_2"."name" AS "name") AS "json_row_2")) AS "json_2" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_0"."tags"))) LIMIT ('20') :: integer) AS "tags_2" LIMIT ('20') :: integer) AS "json_agg_2") AS "tags_2_join" ON ('true') 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(`{ @@ -310,6 +314,43 @@ func nestedInsertOneToOneWithConnect(t *testing.T) { } } +func nestedInsertOneToOneWithConnectArray(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 (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id" = ANY((select a::bigint AS list from json_array_elements_text((i.j->'user'->'connect'->>'id')::json) AS a)) LIMIT 1), "products" AS (INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + + vars := map[string]json.RawMessage{ + "data": json.RawMessage(`{ + "name": "Apple", + "price": 1.25, + "created_at": "now", + "updated_at": "now", + "user": { + "connect": { "id": [1,2] } + } + }`), + } + + 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) @@ -320,4 +361,6 @@ func TestCompileInsert(t *testing.T) { t.Run("nestedInsertOneToOne", nestedInsertOneToOne) t.Run("nestedInsertOneToManyWithConnect", nestedInsertOneToManyWithConnect) t.Run("nestedInsertOneToOneWithConnect", nestedInsertOneToOneWithConnect) + t.Run("nestedInsertOneToOneWithConnectArray", nestedInsertOneToOneWithConnectArray) + } diff --git a/psql/mutate.go b/psql/mutate.go index 94bd6bf..c3c7ed9 100644 --- a/psql/mutate.go +++ b/psql/mutate.go @@ -101,6 +101,9 @@ type renitem struct { data map[string]json.RawMessage } +// TODO: Handle cases where a column name matches the child table name +// the child path needs to be exluded in the json sent to insert or update + func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error { var data map[string]json.RawMessage var array bool @@ -124,9 +127,6 @@ func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error { 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) @@ -152,13 +152,9 @@ func (c *compilerContext) handleKVItem(st *util.Stack, item kvitem) error { id++ } - } else { - ti, err := c.schema.GetTable(k) - if err != nil { - return err - } // Get parent-to-child relationship - relPC, err := c.schema.GetRel(item.key, k) + } else if relPC, err := c.schema.GetRel(item.key, k); err == nil { + ti, err := c.schema.GetTable(k) if err != nil { return err } @@ -277,8 +273,12 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error { io.WriteString(w, ` SET `) quoted(w, item.relPC.Right.Col) io.WriteString(w, ` = `) + + // When setting the id of the connected table in a one-to-many setting + // we always overwrite the value including for array columns colWithTable(w, item.relPC.Left.Table, item.relPC.Left.Col) - io.WriteString(w, `FROM `) + + io.WriteString(w, ` FROM `) quoted(w, item.relPC.Left.Table) io.WriteString(w, ` WHERE`) @@ -290,7 +290,7 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error { } else { io.WriteString(w, ` (`) } - if err := renderKVItemWhere(w, v); err != nil { + if err := renderWhereFromJSON(w, v, "connect", v.val); err != nil { return err } io.WriteString(w, `)`) @@ -313,7 +313,19 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error { quoted(w, item.ti.Name) io.WriteString(w, ` SET `) quoted(w, item.relPC.Right.Col) - io.WriteString(w, ` = NULL`) + io.WriteString(w, ` = `) + + if item.relPC.Right.Array { + io.WriteString(w, ` array_remove(`) + quoted(w, item.relPC.Right.Col) + io.WriteString(w, `, `) + colWithTable(w, item.relPC.Left.Table, item.relPC.Left.Col) + io.WriteString(w, `)`) + + } else { + io.WriteString(w, ` NULL`) + } + io.WriteString(w, ` FROM `) quoted(w, item.relPC.Left.Table) io.WriteString(w, ` WHERE`) @@ -326,7 +338,7 @@ func (c *compilerContext) renderUnionStmt(w io.Writer, item renitem) error { } else { io.WriteString(w, ` (`) } - if err := renderKVItemWhere(w, v); err != nil { + if err := renderWhereFromJSON(w, v, "disconnect", v.val); err != nil { return err } io.WriteString(w, `)`) @@ -514,12 +526,24 @@ func (c *compilerContext) renderConnectStmt(qc *qcode.QCode, w io.Writer, io.WriteString(w, `, `) quoted(w, item.ti.Name) - io.WriteString(c.w, ` AS (`) + io.WriteString(c.w, ` AS (SELECT `) - io.WriteString(c.w, `SELECT * FROM `) + if rel.Left.Array { + io.WriteString(w, `array_agg(DISTINCT `) + quoted(w, rel.Right.Col) + io.WriteString(w, `) AS `) + quoted(w, rel.Right.Col) + + } else { + quoted(w, rel.Right.Col) + + } + + io.WriteString(c.w, ` FROM "_sg_input" i,`) quoted(c.w, item.ti.Name) + io.WriteString(c.w, ` WHERE `) - if err := renderKVItemWhere(c.w, item.kvitem); err != nil { + if err := renderWhereFromJSON(c.w, item.kvitem, "connect", item.kvitem.val); err != nil { return err } io.WriteString(c.w, ` LIMIT 1)`) @@ -540,43 +564,95 @@ func (c *compilerContext) renderDisconnectStmt(qc *qcode.QCode, w io.Writer, quoted(w, item.ti.Name) io.WriteString(c.w, ` AS (`) - io.WriteString(c.w, `SELECT * FROM (VALUES(NULL::`) - io.WriteString(w, rel.Right.col.Type) - io.WriteString(c.w, `)) AS LOOKUP(`) - quoted(w, rel.Right.Col) - io.WriteString(c.w, `))`) + if rel.Right.Array { + io.WriteString(c.w, `SELECT `) + quoted(w, rel.Right.Col) + io.WriteString(c.w, ` FROM "_sg_input" i,`) + quoted(c.w, item.ti.Name) + io.WriteString(c.w, ` WHERE `) + if err := renderWhereFromJSON(c.w, item.kvitem, "connect", item.kvitem.val); err != nil { + return err + } + io.WriteString(c.w, ` LIMIT 1))`) + + } else { + io.WriteString(c.w, `SELECT * FROM (VALUES(NULL::`) + io.WriteString(w, rel.Right.col.Type) + io.WriteString(c.w, `)) AS LOOKUP(`) + quoted(w, rel.Right.Col) + io.WriteString(c.w, `))`) + } return nil } -func renderKVItemWhere(w io.Writer, item kvitem) error { - return renderWhereFromJSON(w, item.ti.Name, item.val) -} - -func renderWhereFromJSON(w io.Writer, table string, val []byte) error { +func renderWhereFromJSON(w io.Writer, item kvitem, key string, val []byte) error { var kv map[string]json.RawMessage + ti := item.ti + if err := json.Unmarshal(val, &kv); err != nil { return err } i := 0 for k, v := range kv { + col, ok := ti.ColMap[k] + if !ok { + continue + } if i != 0 { io.WriteString(w, ` AND `) } - colWithTable(w, table, k) - io.WriteString(w, ` = '`) - switch v[0] { - case '"': - w.Write(v[1 : len(v)-1]) - default: - w.Write(v) + + if v[0] == '[' { + colWithTable(w, ti.Name, k) + + if col.Array { + io.WriteString(w, ` && `) + } else { + io.WriteString(w, ` = `) + } + + io.WriteString(w, `ANY((select a::`) + io.WriteString(w, col.Type) + + io.WriteString(w, ` AS list from json_array_elements_text(`) + renderPathJSON(w, item, key, k) + io.WriteString(w, `::json) AS a))`) + + } else if col.Array { + io.WriteString(w, `(`) + renderPathJSON(w, item, key, k) + io.WriteString(w, `)::`) + io.WriteString(w, col.Type) + + io.WriteString(w, ` = ANY(`) + colWithTable(w, ti.Name, k) + io.WriteString(w, `)`) + + } else { + colWithTable(w, ti.Name, k) + + io.WriteString(w, `= (`) + renderPathJSON(w, item, key, k) + io.WriteString(w, `)::`) + io.WriteString(w, col.Type) } - io.WriteString(w, `'`) + i++ } return nil } +func renderPathJSON(w io.Writer, item kvitem, key1, key2 string) { + io.WriteString(w, `(i.j->`) + joinPath(w, item.path) + io.WriteString(w, `->'`) + io.WriteString(w, key1) + io.WriteString(w, `'->>'`) + io.WriteString(w, key2) + io.WriteString(w, `')`) +} + func renderCteName(w io.Writer, item kvitem) error { io.WriteString(w, `"`) io.WriteString(w, item.ti.Name) diff --git a/psql/schema.go b/psql/schema.go index a948801..3005d9c 100644 --- a/psql/schema.go +++ b/psql/schema.go @@ -136,7 +136,9 @@ func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error { return fmt.Errorf("invalid foreign key table '%s'", ct) } - for _, c := range cols { + for i := range cols { + c := cols[i] + if len(c.FKeyTable) == 0 || len(c.FKeyColID) == 0 { continue } @@ -188,10 +190,12 @@ func (s *DBSchema) updateRelationships(t DBTable, cols []DBColumn) error { rel2 = &DBRel{Type: RelOneToMany} } + rel2.Left.col = fc rel2.Left.Table = c.FKeyTable rel2.Left.Col = fc.Name rel2.Left.Array = fc.Array + rel2.Right.col = &c rel2.Right.Table = t.Name rel2.Right.Col = c.Name rel2.Right.Array = c.Array @@ -249,9 +253,11 @@ func (s *DBSchema) updateSchemaOTMT( rel1.Through = ti.Name rel1.ColT = col2.Name + rel1.Left.col = &col2 rel1.Left.Table = col2.FKeyTable rel1.Left.Col = fc2.Name + rel1.Right.col = &col1 rel1.Right.Table = ti.Name rel1.Right.Col = col1.Name @@ -265,9 +271,11 @@ func (s *DBSchema) updateSchemaOTMT( rel2.Through = ti.Name rel2.ColT = col1.Name + rel1.Left.col = fc1 rel2.Left.Table = col1.FKeyTable rel2.Left.Col = fc1.Name + rel1.Right.col = &col2 rel2.Right.Table = ti.Name rel2.Right.Col = col2.Name diff --git a/psql/update.go b/psql/update.go index 415d676..7b96032 100644 --- a/psql/update.go +++ b/psql/update.go @@ -125,10 +125,10 @@ func (c *compilerContext) renderUpdateStmt(w io.Writer, qc *qcode.QCode, item re if item.relPC.Type == RelOneToMany { if conn, ok := item.data["where"]; ok { io.WriteString(w, ` AND `) - renderWhereFromJSON(w, item.ti.Name, conn) + renderWhereFromJSON(w, item.kvitem, "where", conn) } else if conn, ok := item.data["_where"]; ok { io.WriteString(w, ` AND `) - renderWhereFromJSON(w, item.ti.Name, conn) + renderWhereFromJSON(w, item.kvitem, "_where", conn) } } io.WriteString(w, `)`) @@ -167,7 +167,15 @@ func renderNestedUpdateRelColumns(w io.Writer, item kvitem, values bool) error { if values { colWithTable(w, v.relCP.Left.Table, v.relCP.Left.Col) } else { - quoted(w, v.relCP.Right.Col) + if v.relCP.Right.Array { + io.WriteString(w, `array_remove(`) + colWithTable(w, v.relCP.Left.Table, v.relCP.Left.Col) + io.WriteString(w, `, `) + quoted(w, v.relCP.Right.Col) + io.WriteString(w, `)`) + } else { + quoted(w, v.relCP.Right.Col) + } } } } diff --git a/psql/update_test.go b/psql/update_test.go index ec684f7..24b56f1 100644 --- a/psql/update_test.go +++ b/psql/update_test.go @@ -119,7 +119,7 @@ func nestedUpdateOneToMany(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t) WHERE (("users"."id") IS NOT DISTINCT FROM 8) RETURNING "users".*), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "users" WHERE (("products"."user_id") = ("users"."id") AND "products"."id" = '2') RETURNING "products".*) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + 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") IS NOT DISTINCT FROM 8) RETURNING "users".*), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "users" WHERE (("products"."user_id") = ("users"."id") AND "products"."id"= ((i.j->'product'->'where'->>'id'))::bigint) RETURNING "products".*) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{ @@ -200,7 +200,7 @@ func nestedUpdateOneToManyWithConnect(t *testing.T) { } }` - sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t) WHERE (("users"."id") = 6) RETURNING "users".*), "products_c" AS ( UPDATE "products" SET "user_id" = "users"."id"FROM "users" WHERE ("products"."id" = '7') RETURNING "products".*), "products_d" AS ( UPDATE "products" SET "user_id" = NULL FROM "users" WHERE ("products"."id" = '8') RETURNING "products".*), "products" AS (SELECT * FROM "products_c" UNION ALL SELECT * FROM "products_d") SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t) WHERE (("users"."id") = 6) RETURNING "users".*), "products_c" AS ( UPDATE "products" SET "user_id" = "users"."id" FROM "users" WHERE ("products"."id"= ((i.j->'product'->'connect'->>'id'))::bigint) RETURNING "products".*), "products_d" AS ( UPDATE "products" SET "user_id" = NULL FROM "users" WHERE ("products"."id"= ((i.j->'product'->'disconnect'->>'id'))::bigint) RETURNING "products".*), "products" AS (SELECT * FROM "products_c" UNION ALL SELECT * FROM "products_d") SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{ @@ -238,9 +238,9 @@ func nestedUpdateOneToOneWithConnect(t *testing.T) { } }` - sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM "users" WHERE "users"."id" = '5' AND "users"."email" = 'test@test.com' LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 9) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint AND "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 9) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` - sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM "users" WHERE "users"."email" = 'test@test.com' AND "users"."id" = '5' LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 9) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` + sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT "id" FROM "_sg_input" i,"users" WHERE "users"."email"= ((i.j->'user'->'connect'->>'email'))::character varying AND "users"."id"= ((i.j->'user'->'connect'->>'id'))::bigint LIMIT 1), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 9) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{ @@ -295,6 +295,37 @@ func nestedUpdateOneToOneWithDisconnect(t *testing.T) { } } +// func nestedUpdateOneToOneWithDisconnectArray(t *testing.T) { +// gql := `mutation { +// product(update: $data, id: 2) { +// id +// name +// user_id +// } +// }` + +// sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (SELECT * FROM (VALUES(NULL::bigint)) AS LOOKUP("id")), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "users"."id" FROM "_sg_input" i, "users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 2) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."user_id" AS "user_id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` + +// vars := map[string]json.RawMessage{ +// "data": json.RawMessage(`{ +// "name": "Apple", +// "price": 1.25, +// "user": { +// "disconnect": { "id": 5 } +// } +// }`), +// } + +// resSQL, err := compileGQLToPSQL(gql, vars, "admin") +// if err != nil { +// t.Fatal(err) +// } + +// if string(resSQL) != sql { +// t.Fatal(errNotExpected) +// } +// } + func TestCompileUpdate(t *testing.T) { t.Run("singleUpdate", singleUpdate) t.Run("simpleUpdateWithPresets", simpleUpdateWithPresets) @@ -304,4 +335,5 @@ func TestCompileUpdate(t *testing.T) { t.Run("nestedUpdateOneToManyWithConnect", nestedUpdateOneToManyWithConnect) t.Run("nestedUpdateOneToOneWithConnect", nestedUpdateOneToOneWithConnect) t.Run("nestedUpdateOneToOneWithDisconnect", nestedUpdateOneToOneWithDisconnect) + //t.Run("nestedUpdateOneToOneWithDisconnectArray", nestedUpdateOneToOneWithDisconnectArray) } diff --git a/qcode/parse.go b/qcode/parse.go index 8870543..e4043d1 100644 --- a/qcode/parse.go +++ b/qcode/parse.go @@ -167,11 +167,6 @@ func parseSelectionSet(gql []byte) (*Operation, error) { return nil, fmt.Errorf("invalid '%s' found after closing '}'", p.current()) } - // for i := p.pos; i < len(p.items); i++ { - // fmt.Printf("2>>>> %#v\n", p.items[i]) - // } - //return nil, fmt.Errorf("unexpected token") - lexPool.Put(l) return op, err diff --git a/serv/cmd_serv.go b/serv/cmd_serv.go index 2dcdc4c..2a8062a 100644 --- a/serv/cmd_serv.go +++ b/serv/cmd_serv.go @@ -12,13 +12,15 @@ func cmdServ(cmd *cobra.Command, args []string) { } if conf != nil { - if db, err = initDBPool(conf); err != nil { + db, err = initDBPool(conf) + + if err == nil { + initCompiler() + initAllowList(confPath) + initPreparedList() + } else { fatalInProd(err, "failed to connect to database") } - - initCompiler() - initAllowList(confPath) - initPreparedList() } initWatcher(confPath)