From 7413813138929b650dd33be01b93072c77a8ab21 Mon Sep 17 00:00:00 2001 From: Vikram Rangnekar Date: Mon, 10 Feb 2020 12:15:37 +0530 Subject: [PATCH] Add pagination using opaque cursors --- config/dev.yml | 4 + config/prod.yml | 4 + crypto/encrypt.go | 80 ++++++ docs/guide.md | 69 +++++ jsn/filter.go | 2 +- jsn/get.go | 3 +- jsn/json_test.go | 29 +++ jsn/keys.go | 3 +- jsn/replace.go | 3 +- jsn/strip.go | 3 +- psql/columns.go | 198 ++++++++++++++ psql/insert_test.go | 115 +------- psql/mutate.go | 6 - psql/mutate_test.go | 44 +--- psql/psql_test.go | 101 +++++++- psql/query.go | 620 +++++++++++++------------------------------- psql/query_test.go | 321 ++++------------------- psql/tables.go | 10 + psql/tests.sql | 148 +++++++++++ psql/update_test.go | 97 +------ qcode/parse.go | 27 +- qcode/qcode.go | 183 ++++++++----- serv/cmd.go | 20 +- serv/cmd_serv.go | 1 + serv/config.go | 1 + serv/core.go | 5 + serv/cursor.go | 67 +++++ serv/init.go | 13 + serv/serv.go | 5 +- tmpl/dev.yml | 4 + tmpl/prod.yml | 4 + 31 files changed, 1142 insertions(+), 1048 deletions(-) create mode 100644 crypto/encrypt.go create mode 100644 psql/columns.go create mode 100644 psql/tests.sql create mode 100644 serv/cursor.go diff --git a/config/dev.yml b/config/dev.yml index 3a30596..34cd332 100644 --- a/config/dev.yml +++ b/config/dev.yml @@ -32,6 +32,10 @@ reload_on_config_change: true # Path pointing to where the migrations can be found migrations_path: ./config/migrations +# Secret key for general encryption operations like +# encrypting the cursor data +secret_key: supercalifajalistics + # Postgres related environment Variables # SG_DATABASE_HOST # SG_DATABASE_PORT diff --git a/config/prod.yml b/config/prod.yml index 15d65db..c4d8169 100644 --- a/config/prod.yml +++ b/config/prod.yml @@ -32,6 +32,10 @@ enable_tracing: true # Path pointing to where the migrations can be found # migrations_path: migrations +# Secret key for general encryption operations like +# encrypting the cursor data +# secret_key: supercalifajalistics + # Postgres related environment Variables # SG_DATABASE_HOST # SG_DATABASE_PORT diff --git a/crypto/encrypt.go b/crypto/encrypt.go new file mode 100644 index 0000000..faa0662 --- /dev/null +++ b/crypto/encrypt.go @@ -0,0 +1,80 @@ +// cryptopasta - basic cryptography examples +// +// Written in 2015 by George Tankersley +// +// To the extent possible under law, the author(s) have dedicated all copyright +// and related and neighboring rights to this software to the public domain +// worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication along +// with this software. If not, see // . + +// Provides symmetric authenticated encryption using 256-bit AES-GCM with a random nonce. +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "io" +) + +// NewEncryptionKey generates a random 256-bit key for Encrypt() and +// Decrypt(). It panics if the source of randomness fails. +func NewEncryptionKey() [32]byte { + key := [32]byte{} + _, err := io.ReadFull(rand.Reader, key[:]) + if err != nil { + panic(err) + } + return key +} + +// Encrypt encrypts data using 256-bit AES-GCM. This both hides the content of +// the data and provides a check that it hasn't been altered. Output takes the +// form nonce|ciphertext|tag where '|' indicates concatenation. +func Encrypt(plaintext []byte, key *[32]byte) (ciphertext []byte, err error) { + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, plaintext, nil), nil +} + +// Decrypt decrypts data using 256-bit AES-GCM. This both hides the content of +// the data and provides a check that it hasn't been altered. Expects input +// form nonce|ciphertext|tag where '|' indicates concatenation. +func Decrypt(ciphertext []byte, key *[32]byte) (plaintext []byte, err error) { + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + if len(ciphertext) < gcm.NonceSize() { + return nil, errors.New("malformed ciphertext") + } + + return gcm.Open(nil, + ciphertext[:gcm.NonceSize()], + ciphertext[gcm.NonceSize():], + nil, + ) +} diff --git a/docs/guide.md b/docs/guide.md index 34510ef..404371e 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -1024,6 +1024,75 @@ mutation { } ``` +#### Pagination + +This is a must have feature of any API. When you want your users to go thought a list page by page or implement some fancy infinite scroll you're going to need pagination. There are two ways to paginate in Super Graph. + + Limit-Offset +This is simple enough but also inefficient when working with a large number of total items. Limit, limits the number of items fetched and offset is the point you want to fetch from. The below query will fetch 10 results at a time starting with the 100th item. You will have to keep updating offset (110, 120, 130, etc ) to walk thought the results so make offset a variable. + +```graphql +query { + products(limit: 10, offset: 100) { + id + slug + name + } +} +``` + +#### Cursor +This is a powerful and highly efficient way to paginate though a large number of results. Infact it does not matter how many total results there are this will always be lighting fast. You can use a cursor to walk forward of backward though the results. If you plan to implement infinite scroll this is the option you should choose. + +When going this route the results will contain a cursor value this is an encrypted string that you don't have to worry about just pass this back in to the next API call and you'll received the next set of results. The cursor value is encrypted since its contents should only matter to Super Graph and not the client. Also since the primary key is used for this feature it's possible you might not want to leak it's value to clients. + +You will need to set this config value to ensure the encrypted cursor data is secure. If not set a random value is used which will change with each deployment breaking older cursor values that clients might be using so best to set it. + +```yaml +# Secret key for general encryption operations like +# encrypting the cursor data +secret_key: supercalifajalistics +``` + +Paginating forward through your results + +```graphql +query { + products(first: 10, after: "MJoTLbQF4l0GuoDsYmCrpjPeaaIlNpfm4uFU4PQ=") { + slug + name + } +} +``` + +Paginating backward through your results + +```graphql +query { + products(last: 10, before: "MJoTLbQF4l0GuoDsYmCrpjPeaaIlNpfm4uFU4PQ=") { + slug + name + } +} +``` + +```graphql +"data": { + "products": [ + { + "slug": "eius-nulla-et-8", + "name" "Pale Ale" + }, + { + "slug": "sapiente-ut-alias-12", + "name" "Brown Ale" + } + ... + ], + "products_cursor": "dJwHassm5+d82rGydH2xQnwNxJ1dcj4/cxkh5Cer" +} +``` + ## Using Variables Variables (`$product_id`) and their values (`"product_id": 5`) can be passed along side the GraphQL query. Using variables makes for better client side code as well as improved server side SQL query caching. The build-in web-ui also supports setting variables. Not having to manipulate your GraphQL query string to insert values into it makes for cleaner diff --git a/jsn/filter.go b/jsn/filter.go index 582726e..d43adc6 100644 --- a/jsn/filter.go +++ b/jsn/filter.go @@ -109,7 +109,7 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error { case state == expectValue && b[i] == 'n': state = expectNull - case state == expectNull && b[i] == 'l': + case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'): e = i } diff --git a/jsn/get.go b/jsn/get.go index 04f8419..ed8e2c8 100644 --- a/jsn/get.go +++ b/jsn/get.go @@ -117,8 +117,9 @@ func Get(b []byte, keys [][]byte) []Field { case state == expectValue && b[i] == 'n': state = expectNull + s = i - case state == expectNull && b[i] == 'l': + case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'): e = i } diff --git a/jsn/json_test.go b/jsn/json_test.go index 56bd9f4..7baf4a8 100644 --- a/jsn/json_test.go +++ b/jsn/json_test.go @@ -158,6 +158,9 @@ var ( input5 = ` {"data":{"title":"In September 2018, Slovak police stated that Kuciak was murdered because of his investigative work, and that the murder had been ordered.[9][10] They arrested eight suspects,[11] charging three of them with first-degree murder.[11]","topics":["cpp"]},"a":["1111"]},"thread_slug":"in-september-2018-slovak-police-stated-that-kuciak-7929",}` + + input6 = ` + {"users" : [{"id" : 1, "email" : "vicram@gmail.com", "slug" : "vikram-rangnekar", "threads" : [], "threads_cursor" : null}, {"id" : 3, "email" : "marareilly@lang.name", "slug" : "raymundo-corwin", "threads" : [{"id" : 9, "title" : "Et alias et aut porro praesentium nam in voluptatem reiciendis quisquam perspiciatis inventore eos quia et et enim qui amet."}, {"id" : 25, "title" : "Ipsam quam nemo culpa tempore amet optio sit sed eligendi autem consequatur quaerat rem velit quibusdam quibusdam optio a voluptatem."}], "threads_cursor" : 25}], "users_cursor" : 3}` ) func TestGet(t *testing.T) { @@ -227,6 +230,32 @@ func TestGet1(t *testing.T) { } } +func TestGet2(t *testing.T) { + values := Get([]byte(input6), [][]byte{ + []byte("users_cursor"), []byte("threads_cursor"), + }) + + expected := []Field{ + {[]byte("threads_cursor"), []byte(`null`)}, + {[]byte("threads_cursor"), []byte(`25`)}, + {[]byte("users_cursor"), []byte(`3`)}, + } + + if len(values) != len(expected) { + t.Fatal("len(values) != len(expected)") + } + + for i := range expected { + if !bytes.Equal(values[i].Key, expected[i].Key) { + t.Error(string(values[i].Key), " != ", string(expected[i].Key)) + } + + if !bytes.Equal(values[i].Value, expected[i].Value) { + t.Error(string(values[i].Value), " != ", string(expected[i].Value)) + } + } +} + func TestValue(t *testing.T) { v1 := []byte("12345") if !bytes.Equal(Value(v1), v1) { diff --git a/jsn/keys.go b/jsn/keys.go index 3c9d54c..e89f9ab 100644 --- a/jsn/keys.go +++ b/jsn/keys.go @@ -101,8 +101,9 @@ func Keys(b []byte) [][]byte { case state == expectValue && b[i] == 'n': state = expectNull + s = i - case state == expectNull && b[i] == 'l': + case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'): e = i } diff --git a/jsn/replace.go b/jsn/replace.go index 44eddc4..b538d9b 100644 --- a/jsn/replace.go +++ b/jsn/replace.go @@ -104,8 +104,9 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error { case state == expectValue && b[i] == 'n': state = expectNull + s = i - case state == expectNull && b[i] == 'l': + case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'): e = i } diff --git a/jsn/strip.go b/jsn/strip.go index 827cdf7..305967d 100644 --- a/jsn/strip.go +++ b/jsn/strip.go @@ -82,8 +82,9 @@ func Strip(b []byte, path [][]byte) []byte { case state == expectValue && b[i] == 'n': state = expectNull + s = i - case state == expectNull && b[i] == 'l': + case state == expectNull && (b[i-1] == 'l' && b[i] == 'l'): e = i } diff --git a/psql/columns.go b/psql/columns.go new file mode 100644 index 0000000..4dd4245 --- /dev/null +++ b/psql/columns.go @@ -0,0 +1,198 @@ +//nolint:errcheck +package psql + +import ( + "errors" + "io" + "strings" + + "github.com/dosco/super-graph/qcode" +) + +func (c *compilerContext) renderBaseColumns( + sel *qcode.Select, + ti *DBTableInfo, + childCols []*qcode.Column, + skipped uint32) ([]int, bool, error) { + + var realColsRendered []int + + colcount := (len(sel.Cols) + len(sel.OrderBy) + 1) + colmap := make(map[string]struct{}, colcount) + + isSearch := sel.Args["search"] != nil + isCursorPaged := sel.Paging.Type != qcode.PtOffset + isAgg := false + + i := 0 + for n, col := range sel.Cols { + cn := col.Name + colmap[cn] = struct{}{} + + _, isRealCol := ti.ColMap[cn] + + if isRealCol { + c.renderComma(i) + realColsRendered = append(realColsRendered, n) + colWithTable(c.w, ti.Name, cn) + i++ + continue + } + + if isSearch && !isRealCol { + switch { + case cn == "search_rank": + if err := c.renderColumnSearchRank(sel, ti, col, i); err != nil { + return nil, false, err + } + i++ + + case strings.HasPrefix(cn, "search_headline_"): + if err := c.renderColumnSearchHeadline(sel, ti, col, i); err != nil { + return nil, false, err + } + i++ + + } + } else { + if err := c.renderColumnFunction(sel, ti, col, i); err != nil { + return nil, false, err + } + isAgg = true + i++ + + } + } + + if isCursorPaged { + if _, ok := colmap[ti.PrimaryCol.Key]; !ok { + colmap[ti.PrimaryCol.Key] = struct{}{} + c.renderComma(i) + colWithTable(c.w, ti.Name, ti.PrimaryCol.Name) + } + i++ + } + + for _, ob := range sel.OrderBy { + if _, ok := colmap[ob.Col]; ok { + continue + } + colmap[ob.Col] = struct{}{} + c.renderComma(i) + colWithTable(c.w, ti.Name, ob.Col) + i++ + } + + for _, col := range childCols { + if _, ok := colmap[col.Name]; ok { + continue + } + c.renderComma(i) + colWithTable(c.w, col.Table, col.Name) + i++ + } + + return realColsRendered, isAgg, nil +} + +func (c *compilerContext) renderColumnSearchRank(sel *qcode.Select, ti *DBTableInfo, col qcode.Column, columnsRendered int) error { + if isColumnBlocked(sel, col.Name) { + return nil + } + + if ti.TSVCol == nil { + return errors.New("no ts_vector column found") + } + cn := ti.TSVCol.Name + arg := sel.Args["search"] + + c.renderComma(columnsRendered) + //fmt.Fprintf(w, `ts_rank("%s"."%s", websearch_to_tsquery('%s')) AS %s`, + //c.sel.Name, cn, arg.Val, col.Name) + io.WriteString(c.w, `ts_rank(`) + colWithTable(c.w, ti.Name, cn) + if c.schema.ver >= 110000 { + io.WriteString(c.w, `, websearch_to_tsquery('`) + } else { + io.WriteString(c.w, `, to_tsquery('`) + } + io.WriteString(c.w, arg.Val) + io.WriteString(c.w, `'))`) + alias(c.w, col.Name) + + return nil +} + +func (c *compilerContext) renderColumnSearchHeadline(sel *qcode.Select, ti *DBTableInfo, col qcode.Column, columnsRendered int) error { + cn := col.Name[16:] + + if isColumnBlocked(sel, cn) { + return nil + } + arg := sel.Args["search"] + + c.renderComma(columnsRendered) + //fmt.Fprintf(w, `ts_headline("%s"."%s", websearch_to_tsquery('%s')) AS %s`, + //c.sel.Name, cn, arg.Val, col.Name) + io.WriteString(c.w, `ts_headline(`) + colWithTable(c.w, ti.Name, cn) + if c.schema.ver >= 110000 { + io.WriteString(c.w, `, websearch_to_tsquery('`) + } else { + io.WriteString(c.w, `, to_tsquery('`) + } + io.WriteString(c.w, arg.Val) + io.WriteString(c.w, `'))`) + alias(c.w, col.Name) + + return nil +} + +func (c *compilerContext) renderColumnFunction(sel *qcode.Select, ti *DBTableInfo, col qcode.Column, columnsRendered int) error { + pl := funcPrefixLen(col.Name) + // if pl == 0 { + // //fmt.Fprintf(w, `'%s not defined' AS %s`, cn, col.Name) + // io.WriteString(c.w, `'`) + // io.WriteString(c.w, col.Name) + // io.WriteString(c.w, ` not defined'`) + // alias(c.w, col.Name) + // } + + if pl == 0 || !sel.Functions { + return nil + } + + cn := col.Name[pl:] + + if isColumnBlocked(sel, cn) { + return nil + } + + fn := cn[0 : pl-1] + + c.renderComma(columnsRendered) + + //fmt.Fprintf(w, `%s("%s"."%s") AS %s`, fn, c.sel.Name, cn, col.Name) + io.WriteString(c.w, fn) + io.WriteString(c.w, `(`) + colWithTable(c.w, ti.Name, cn) + io.WriteString(c.w, `)`) + alias(c.w, col.Name) + + return nil +} + +func (c *compilerContext) renderComma(columnsRendered int) { + if columnsRendered != 0 { + io.WriteString(c.w, `, `) + } +} + +func isColumnBlocked(sel *qcode.Select, name string) bool { + if len(sel.Allowed) != 0 { + if _, ok := sel.Allowed[name]; !ok { + return true + } + } + return false +} diff --git a/psql/insert_test.go b/psql/insert_test.go index 6801039..27f0866 100644 --- a/psql/insert_test.go +++ b/psql/insert_test.go @@ -12,20 +12,11 @@ func simpleInsert(t *testing.T) { } }` - 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) - } + compileGQLToPSQL(t, gql, vars, "user") } func singleInsert(t *testing.T) { @@ -36,20 +27,11 @@ func singleInsert(t *testing.T) { } }` - 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) - } + compileGQLToPSQL(t, gql, vars, "anon") } func bulkInsert(t *testing.T) { @@ -60,20 +42,11 @@ func bulkInsert(t *testing.T) { } }` - 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) - } + compileGQLToPSQL(t, gql, vars, "anon") } func simpleInsertWithPresets(t *testing.T) { @@ -83,20 +56,11 @@ func simpleInsertWithPresets(t *testing.T) { } }` - 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) - } + compileGQLToPSQL(t, gql, vars, "user") } func nestedInsertManyToMany(t *testing.T) { @@ -118,10 +82,6 @@ func nestedInsertManyToMany(t *testing.T) { } }` - 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", @@ -139,16 +99,7 @@ func nestedInsertManyToMany(t *testing.T) { `), } - for i := 0; i < 1000; i++ { - resSQL, err := compileGQLToPSQL(gql, vars, "admin") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql1 && string(resSQL) != sql2 { - t.Fatal(errNotExpected) - } - } + compileGQLToPSQL(t, gql, vars, "admin") } func nestedInsertOneToMany(t *testing.T) { @@ -165,8 +116,6 @@ func nestedInsertOneToMany(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 (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", @@ -182,14 +131,7 @@ func nestedInsertOneToMany(t *testing.T) { }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "admin") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, vars, "admin") } func nestedInsertOneToOne(t *testing.T) { @@ -205,8 +147,6 @@ func nestedInsertOneToOne(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->'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", @@ -225,14 +165,7 @@ func nestedInsertOneToOne(t *testing.T) { }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "admin") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, vars, "admin") } func nestedInsertOneToManyWithConnect(t *testing.T) { @@ -249,8 +182,6 @@ 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"= ((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(`{ "email": "thedude@rug.com", @@ -263,14 +194,7 @@ func nestedInsertOneToManyWithConnect(t *testing.T) { }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "admin") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, vars, "admin") } func nestedInsertOneToOneWithConnect(t *testing.T) { @@ -290,8 +214,6 @@ func nestedInsertOneToOneWithConnect(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_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", "_x_users"."id" FROM "_sg_input" i, "_x_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(`{ "name": "Apple", @@ -304,14 +226,7 @@ func nestedInsertOneToOneWithConnect(t *testing.T) { }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "admin") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, vars, "admin") } func nestedInsertOneToOneWithConnectArray(t *testing.T) { @@ -327,8 +242,6 @@ func nestedInsertOneToOneWithConnectArray(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_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", "_x_users"."id" FROM "_sg_input" i, "_x_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", @@ -341,14 +254,7 @@ func nestedInsertOneToOneWithConnectArray(t *testing.T) { }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "admin") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, vars, "admin") } func TestCompileInsert(t *testing.T) { @@ -362,5 +268,4 @@ func TestCompileInsert(t *testing.T) { t.Run("nestedInsertOneToManyWithConnect", nestedInsertOneToManyWithConnect) t.Run("nestedInsertOneToOneWithConnect", nestedInsertOneToOneWithConnect) t.Run("nestedInsertOneToOneWithConnectArray", nestedInsertOneToOneWithConnectArray) - } diff --git a/psql/mutate.go b/psql/mutate.go index eab9b5c..08dad58 100644 --- a/psql/mutate.go +++ b/psql/mutate.go @@ -682,12 +682,6 @@ func renderCteNameWithSuffix(w io.Writer, item kvitem, suffix string) error { return nil } -func quoted(w io.Writer, identifier string) { - io.WriteString(w, `"`) - io.WriteString(w, identifier) - io.WriteString(w, `"`) -} - func joinPath(w io.Writer, path []string) { for i := range path { if i != 0 { diff --git a/psql/mutate_test.go b/psql/mutate_test.go index e4216d7..c0a7fc2 100644 --- a/psql/mutate_test.go +++ b/psql/mutate_test.go @@ -13,20 +13,11 @@ func singleUpsert(t *testing.T) { } }` - 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{ "upsert": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, vars, "user") } func singleUpsertWhere(t *testing.T) { @@ -37,20 +28,11 @@ func singleUpsertWhere(t *testing.T) { } }` - 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{ "upsert": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, vars, "user") } func bulkUpsert(t *testing.T) { @@ -61,20 +43,11 @@ func bulkUpsert(t *testing.T) { } }` - 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{ "upsert": json.RawMessage(` [{ "name": "my_name", "description": "my_desc" }]`), } - resSQL, err := compileGQLToPSQL(gql, vars, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, vars, "user") } func delete(t *testing.T) { @@ -85,20 +58,11 @@ func delete(t *testing.T) { } }` - sql := `WITH "products" AS (DELETE FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8)) AND (("products"."id") IS NOT DISTINCT FROM 1)) RETURNING "products".*)SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` - vars := map[string]json.RawMessage{ "update": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, vars, "user") } // func blockedInsert(t *testing.T) { diff --git a/psql/psql_test.go b/psql/psql_test.go index be03255..083180e 100644 --- a/psql/psql_test.go +++ b/psql/psql_test.go @@ -1,8 +1,11 @@ package psql import ( + "fmt" + "io/ioutil" "log" "os" + "strings" "testing" "github.com/dosco/super-graph/qcode" @@ -10,11 +13,14 @@ import ( const ( errNotExpected = "Generated SQL did not match what was expected" + headerMarker = "=== RUN" + commentMarker = "---" ) var ( qcompile *qcode.Compiler pcompile *Compiler + expected map[string][]string ) func TestMain(m *testing.M) { @@ -138,21 +144,94 @@ func TestMain(m *testing.M) { Vars: vars, }) + expected = make(map[string][]string) + + b, err := ioutil.ReadFile("tests.sql") + if err != nil { + log.Fatal(err) + } + text := string(b) + lines := strings.Split(text, "\n") + + var h string + + for _, v := range lines { + switch { + case strings.HasPrefix(v, headerMarker): + h = strings.TrimSpace(v[len(headerMarker):]) + + case strings.HasPrefix(v, commentMarker): + break + + default: + v := strings.TrimSpace(v) + if len(v) != 0 { + expected[h] = append(expected[h], v) + } + } + } os.Exit(m.Run()) } -func compileGQLToPSQL(gql string, vars Variables, role string) ([]byte, error) { - qc, err := qcompile.Compile([]byte(gql), role) - if err != nil { - return nil, err +func compileGQLToPSQL(t *testing.T, gql string, vars Variables, role string) { + generateTestFile := false + + if generateTestFile { + var sqlStmts []string + + for i := 0; i < 100; i++ { + qc, err := qcompile.Compile([]byte(gql), role) + if err != nil { + t.Fatal(err) + } + + _, sqlB, err := pcompile.CompileEx(qc, vars) + if err != nil { + t.Fatal(err) + } + + sql := string(sqlB) + + match := false + for _, s := range sqlStmts { + if sql == s { + match = true + break + } + } + + if !match { + s := string(sql) + sqlStmts = append(sqlStmts, s) + fmt.Println(s) + } + } + + return } - _, sqlStmt, err := pcompile.CompileEx(qc, vars) - if err != nil { - return nil, err + for i := 0; i < 200; i++ { + qc, err := qcompile.Compile([]byte(gql), role) + if err != nil { + t.Fatal(err) + } + + _, sqlStmt, err := pcompile.CompileEx(qc, vars) + if err != nil { + t.Fatal(err) + } + + failed := true + + for _, sql := range expected[t.Name()] { + if string(sqlStmt) == sql { + failed = false + } + } + + if failed { + fmt.Println(string(sqlStmt)) + t.Fatal(errNotExpected) + } } - - //fmt.Println(string(sqlStmt)) - - return sqlStmt, nil } diff --git a/psql/query.go b/psql/query.go index 73dd9d0..e9f6cc2 100644 --- a/psql/query.go +++ b/psql/query.go @@ -20,17 +20,23 @@ const ( type Variables map[string]json.RawMessage type Config struct { - Schema *DBSchema - Vars map[string]string + Schema *DBSchema + Decryptor func(string) ([]byte, error) + Vars map[string]string } type Compiler struct { - schema *DBSchema - vars map[string]string + schema *DBSchema + decryptor func(string) ([]byte, error) + vars map[string]string } func NewCompiler(conf Config) *Compiler { - return &Compiler{conf.Schema, conf.Vars} + return &Compiler{ + schema: conf.Schema, + decryptor: conf.Decryptor, + vars: conf.Vars, + } } func (c *Compiler) AddRelationship(child, parent string, rel *DBRel) error { @@ -79,61 +85,45 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) { } c := &compilerContext{w, qc.Selects, co} - multiRoot := (len(qc.Roots) > 1) st := NewIntStack() - si := 0 + i := 0 - if multiRoot { - io.WriteString(c.w, `SELECT row_to_json("json_root") FROM (SELECT `) - - for _, id := range qc.Roots { - root := qc.Selects[id] - if root.SkipRender { - continue - } - - st.Push(root.ID + closeBlock) - st.Push(root.ID) - - if si != 0 { - io.WriteString(c.w, `, `) - } - - io.WriteString(c.w, `"sel_`) - int2string(c.w, root.ID) - io.WriteString(c.w, `"."json_`) - int2string(c.w, root.ID) - io.WriteString(c.w, `"`) - - alias(c.w, root.FieldName) - si++ + io.WriteString(c.w, `SELECT json_build_object(`) + for _, id := range qc.Roots { + root := qc.Selects[id] + if root.SkipRender { + continue } - if si != 0 { - io.WriteString(c.w, ` FROM `) + st.Push(root.ID + closeBlock) + st.Push(root.ID) + if i != 0 { + io.WriteString(c.w, `, `) } - } else { - root := qc.Selects[0] - if !root.SkipRender { - io.WriteString(c.w, `SELECT json_object_agg(`) - io.WriteString(c.w, `'`) + io.WriteString(c.w, `'`) + io.WriteString(c.w, root.FieldName) + io.WriteString(c.w, `', `) + io.WriteString(c.w, `"sel_`) + int2string(c.w, root.ID) + io.WriteString(c.w, `"."json"`) + + if root.Paging.Type != qcode.PtOffset { + io.WriteString(c.w, `, '`) io.WriteString(c.w, root.FieldName) - io.WriteString(c.w, `', `) - io.WriteString(c.w, `json_`) + io.WriteString(c.w, `_cursor', "sel_`) int2string(c.w, root.ID) - - st.Push(root.ID + closeBlock) - st.Push(root.ID) - - io.WriteString(c.w, `) FROM `) - si++ + io.WriteString(c.w, `"."__cursor"`) } + + i++ } - if si == 0 { + io.WriteString(c.w, `) as "__root" FROM `) + + if i == 0 { return 0, errors.New("all tables skipped. cannot render query") } @@ -149,19 +139,15 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) { if id < closeBlock { sel := &c.s[id] - if sel.ParentID == -1 { - io.WriteString(c.w, `(`) - } - ti, err := c.schema.GetTable(sel.Name) if err != nil { return 0, err } - if sel.ParentID != -1 { - if err = c.renderLateralJoin(sel); err != nil { - return 0, err - } + if sel.ParentID == -1 { + io.WriteString(c.w, `(`) + } else { + c.renderLateralJoin(sel) } skipped, err := c.renderSelect(sel, ti) @@ -186,45 +172,31 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) { } else { sel := &c.s[(id - closeBlock)] - ti, err := c.schema.GetTable(sel.Name) - if err != nil { - return 0, err - } - - err = c.renderSelectClose(sel, ti) - if err != nil { - return 0, err - } - - if sel.ParentID != -1 { - if err = c.renderLateralJoinClose(sel); err != nil { - return 0, err - } - } else { + if sel.ParentID == -1 { io.WriteString(c.w, `)`) aliasWithID(c.w, `sel`, sel.ID) if st.Len() != 0 { io.WriteString(c.w, `, `) } + } else { + c.renderLateralJoinClose(sel) } if len(sel.Args) != 0 { + i := 0 for _, v := range sel.Args { - qcode.FreeNode(v) + qcode.FreeNode(v, 500) + i++ } } } } - if multiRoot { - io.WriteString(c.w, `) AS "json_root"`) - } - return ignored, nil } -func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (uint32, []*qcode.Column, error) { +func (c *compilerContext) initSelector(sel *qcode.Select, ti *DBTableInfo) (uint32, []*qcode.Column, error) { var skipped uint32 cols := make([]*qcode.Column, 0, len(sel.Cols)) @@ -238,6 +210,40 @@ func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (u colmap[sel.OrderBy[i].Col] = struct{}{} } + if sel.Paging.Type != qcode.PtOffset { + colmap[ti.PrimaryCol.Key] = struct{}{} + + var filOrder qcode.Order + var filOp qcode.ExpOp + + switch sel.Paging.Type { + case qcode.PtForward: + filOrder = qcode.OrderAsc + filOp = qcode.OpGreaterThan + + case qcode.PtBackward: + filOrder = qcode.OrderDesc + filOp = qcode.OpLesserThan + } + + sel.OrderBy = append(sel.OrderBy, &qcode.OrderBy{ + Col: ti.PrimaryCol.Name, + Order: filOrder, + }) + + if len(sel.Paging.Cursor) != 0 { + v, err := c.decryptor(sel.Paging.Cursor) + if err != nil { + return 0, nil, err + } + + fil := qcode.AddFilter(sel) + fil.Op = filOp + fil.Col = ti.PrimaryCol.Name + fil.Val = string(v) + } + } + for _, id := range sel.Children { child := &c.s[id] @@ -296,131 +302,58 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint } } - skipped, childCols, err := c.processChildren(sel, ti) + skipped, childCols, err := c.initSelector(sel, ti) if err != nil { return 0, err } - hasOrder := len(sel.OrderBy) != 0 // SELECT if !ti.Singular { - //fmt.Fprintf(w, `SELECT coalesce(json_agg("%s"`, c.sel.Name) - io.WriteString(c.w, `SELECT coalesce(json_agg("`) - io.WriteString(c.w, "json_") - int2string(c.w, sel.ID) - io.WriteString(c.w, `"`) + io.WriteString(c.w, `SELECT coalesce(json_agg(json_build_object(`) + if err := c.renderColumns(sel, ti, skipped); err != nil { + return 0, err + } + io.WriteString(c.w, `)), '[]') AS "json"`) - if hasOrder { - if err := c.renderOrderBy(sel, ti); err != nil { - return 0, err - } + if sel.Paging.Type != qcode.PtOffset { + io.WriteString(c.w, `, max(`) + colWithTableID(c.w, ti.Name, sel.ID, ti.PrimaryCol.Name) + io.WriteString(c.w, `) AS "__cursor"`) } - //fmt.Fprintf(w, `), '[]') AS "%s" FROM (`, c.sel.Name) - io.WriteString(c.w, `), '[]')`) - aliasWithID(c.w, "json", sel.ID) - io.WriteString(c.w, ` FROM (`) + } else { + io.WriteString(c.w, `SELECT json_build_object(`) + if err := c.renderColumns(sel, ti, skipped); err != nil { + return 0, err + } + io.WriteString(c.w, `) AS "json"`) } - // ROW-TO-JSON - io.WriteString(c.w, `SELECT `) - - if len(sel.DistinctOn) != 0 { - c.renderDistinctOn(sel, ti) - } - - io.WriteString(c.w, `row_to_json((`) - - //fmt.Fprintf(w, `SELECT "%d" FROM (SELECT `, c.sel.ID) - io.WriteString(c.w, `SELECT "json_row_`) - int2string(c.w, sel.ID) - io.WriteString(c.w, `" FROM (SELECT `) - - // Combined column names - c.renderColumns(sel, ti) - - c.renderRemoteRelColumns(sel, ti) - - if err = c.renderJoinedColumns(sel, ti, skipped); err != nil { - return skipped, err - } - - //fmt.Fprintf(w, `) AS "%d"`, c.sel.ID) - io.WriteString(c.w, `)`) - aliasWithID(c.w, "json_row", sel.ID) - - //fmt.Fprintf(w, `)) AS "%s"`, c.sel.Name) - io.WriteString(c.w, `))`) - aliasWithID(c.w, "json", sel.ID) - // END-ROW-TO-JSON - - if hasOrder { - c.renderOrderByColumns(sel, ti) - } - // END-SELECT + io.WriteString(c.w, ` FROM (`) // FROM (SELECT .... ) err = c.renderBaseSelect(sel, ti, rel, childCols, skipped) if err != nil { return skipped, err } + + //fmt.Fprintf(w, `) AS "%s_%d"`, c.sel.Name, c.sel.ID) + io.WriteString(c.w, `)`) + aliasWithID(c.w, ti.Name, sel.ID) + // END-FROM return skipped, nil } -func (c *compilerContext) renderSelectClose(sel *qcode.Select, ti *DBTableInfo) error { - hasOrder := len(sel.OrderBy) != 0 - - if hasOrder { - err := c.renderOrderBy(sel, ti) - if err != nil { - return err - } - } - - switch { - case ti.Singular: - io.WriteString(c.w, ` LIMIT ('1') :: integer`) - - case len(sel.Paging.Limit) != 0: - //fmt.Fprintf(w, ` LIMIT ('%s') :: integer`, c.sel.Paging.Limit) - io.WriteString(c.w, ` LIMIT ('`) - io.WriteString(c.w, sel.Paging.Limit) - io.WriteString(c.w, `') :: integer`) - - case sel.Paging.NoLimit: - break - - default: - io.WriteString(c.w, ` LIMIT ('20') :: integer`) - } - - if len(sel.Paging.Offset) != 0 { - //fmt.Fprintf(w, ` OFFSET ('%s') :: integer`, c.sel.Paging.Offset) - io.WriteString(c.w, `OFFSET ('`) - io.WriteString(c.w, sel.Paging.Offset) - io.WriteString(c.w, `') :: integer`) - } - - if !ti.Singular { - //fmt.Fprintf(w, `) AS "json_agg_%d"`, c.sel.ID) - io.WriteString(c.w, `)`) - aliasWithID(c.w, "json_agg", sel.ID) - } - - return nil -} - func (c *compilerContext) renderLateralJoin(sel *qcode.Select) error { io.WriteString(c.w, ` LEFT OUTER JOIN LATERAL (`) return nil } func (c *compilerContext) renderLateralJoinClose(sel *qcode.Select) error { - //fmt.Fprintf(w, `) AS "%s_%d_join" ON ('true')`, c.sel.Name, c.sel.ID) - io.WriteString(c.w, `)`) - aliasWithIDSuffix(c.w, sel.Name, sel.ID, "_join") + io.WriteString(c.w, `) `) + aliasWithID(c.w, "sel", sel.ID) io.WriteString(c.w, ` ON ('true')`) return nil } @@ -460,7 +393,7 @@ func (c *compilerContext) renderJoinByName(table, parent string, id int32) error return nil } -func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo) { +func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32) error { i := 0 for _, col := range sel.Cols { n := funcPrefixLen(col.Name) @@ -484,15 +417,21 @@ func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo) { if i != 0 { io.WriteString(c.w, ", ") } - //fmt.Fprintf(w, `"%s_%d"."%s" AS "%s"`, - //c.sel.Name, c.sel.ID, col.Name, col.FieldName) - colWithTableIDAlias(c.w, ti.Name, sel.ID, col.Name, col.FieldName) + + squoted(c.w, col.FieldName) + io.WriteString(c.w, ", ") + colWithTableID(c.w, ti.Name, sel.ID, col.Name) + i++ } + + i += c.renderRemoteRelColumns(sel, ti, i) + + return c.renderJoinedColumns(sel, ti, skipped, i) } -func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableInfo) { - i := 0 +func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableInfo, colsRendered int) int { + i := colsRendered for _, id := range sel.Children { child := &c.s[id] @@ -504,18 +443,19 @@ func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableI if i != 0 || len(sel.Cols) != 0 { io.WriteString(c.w, ", ") } - //fmt.Fprintf(w, `"%s_%d"."%s" AS "%s"`, - //c.sel.Name, c.sel.ID, rel.Left.Col, rel.Right.Col) + + squoted(c.w, rel.Right.Col) + io.WriteString(c.w, ", ") colWithTableID(c.w, ti.Name, sel.ID, rel.Left.Col) - alias(c.w, rel.Right.Col) i++ } + + return i } -func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32) error { - +func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32, colsRendered int) error { // columns previously rendered - i := len(sel.Cols) + i := colsRendered for _, id := range sel.Children { if hasBit(skipped, uint32(id)) { @@ -530,18 +470,19 @@ func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo io.WriteString(c.w, ", ") } - //fmt.Fprintf(w, `"%s_%d_join"."%s" AS "%s"`, - //s.Name, s.ID, s.Name, s.FieldName) - //if cti.Singular { - io.WriteString(c.w, `"`) - io.WriteString(c.w, childSel.Name) - io.WriteString(c.w, `_`) + squoted(c.w, childSel.FieldName) + io.WriteString(c.w, `, "sel_`) int2string(c.w, childSel.ID) - io.WriteString(c.w, `_join"."json_`) - int2string(c.w, childSel.ID) - io.WriteString(c.w, `" AS "`) - io.WriteString(c.w, childSel.FieldName) - io.WriteString(c.w, `"`) + io.WriteString(c.w, `"."json"`) + + if childSel.Paging.Type != qcode.PtOffset { + io.WriteString(c.w, `, '`) + io.WriteString(c.w, childSel.FieldName) + io.WriteString(c.w, `_cursor', "sel_`) + int2string(c.w, childSel.ID) + io.WriteString(c.w, `"."__cursor"`) + } + i++ } @@ -550,171 +491,25 @@ func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, rel *DBRel, childCols []*qcode.Column, skipped uint32) error { - var groupBy []int - isRoot := (rel == nil) isFil := (sel.Where != nil && sel.Where.Op != qcode.OpNop) - isSearch := sel.Args["search"] != nil - isAgg := false + hasOrder := len(sel.OrderBy) != 0 - colmap := make(map[string]struct{}, (len(sel.Cols) + len(sel.OrderBy))) + io.WriteString(c.w, `SELECT `) - io.WriteString(c.w, ` FROM (SELECT `) - - i := 0 - for n, col := range sel.Cols { - cn := col.Name - colmap[cn] = struct{}{} - - _, isRealCol := ti.ColMap[cn] - - if !isRealCol { - if isSearch { - switch { - case cn == "search_rank": - if len(sel.Allowed) != 0 { - if _, ok := sel.Allowed[cn]; !ok { - continue - } - } - if ti.TSVCol == nil { - return errors.New("no ts_vector column found") - } - cn = ti.TSVCol.Name - arg := sel.Args["search"] - - if i != 0 { - io.WriteString(c.w, `, `) - } - //fmt.Fprintf(w, `ts_rank("%s"."%s", websearch_to_tsquery('%s')) AS %s`, - //c.sel.Name, cn, arg.Val, col.Name) - io.WriteString(c.w, `ts_rank(`) - colWithTable(c.w, ti.Name, cn) - if c.schema.ver >= 110000 { - io.WriteString(c.w, `, websearch_to_tsquery('`) - } else { - io.WriteString(c.w, `, to_tsquery('`) - } - io.WriteString(c.w, arg.Val) - io.WriteString(c.w, `'))`) - alias(c.w, col.Name) - i++ - - case strings.HasPrefix(cn, "search_headline_"): - cn1 := cn[16:] - if len(sel.Allowed) != 0 { - if _, ok := sel.Allowed[cn1]; !ok { - continue - } - } - arg := sel.Args["search"] - - if i != 0 { - io.WriteString(c.w, `, `) - } - //fmt.Fprintf(w, `ts_headline("%s"."%s", websearch_to_tsquery('%s')) AS %s`, - //c.sel.Name, cn, arg.Val, col.Name) - io.WriteString(c.w, `ts_headline(`) - colWithTable(c.w, ti.Name, cn1) - if c.schema.ver >= 110000 { - io.WriteString(c.w, `, websearch_to_tsquery('`) - } else { - io.WriteString(c.w, `, to_tsquery('`) - } - io.WriteString(c.w, arg.Val) - io.WriteString(c.w, `'))`) - alias(c.w, col.Name) - i++ - - } - } else { - pl := funcPrefixLen(cn) - if pl == 0 { - if i != 0 { - io.WriteString(c.w, `, `) - } - //fmt.Fprintf(w, `'%s not defined' AS %s`, cn, col.Name) - io.WriteString(c.w, `'`) - io.WriteString(c.w, cn) - io.WriteString(c.w, ` not defined'`) - alias(c.w, col.Name) - i++ - - } else if sel.Functions { - cn1 := cn[pl:] - if len(sel.Allowed) != 0 { - if _, ok := sel.Allowed[cn1]; !ok { - continue - } - } - if i != 0 { - io.WriteString(c.w, `, `) - } - fn := cn[0 : pl-1] - isAgg = true - - //fmt.Fprintf(w, `%s("%s"."%s") AS %s`, fn, c.sel.Name, cn, col.Name) - io.WriteString(c.w, fn) - io.WriteString(c.w, `(`) - colWithTable(c.w, ti.Name, cn1) - io.WriteString(c.w, `)`) - alias(c.w, col.Name) - i++ - - } - } - } else { - groupBy = append(groupBy, n) - //fmt.Fprintf(w, `"%s"."%s"`, c.sel.Name, cn) - if i != 0 { - io.WriteString(c.w, `, `) - } - colWithTable(c.w, ti.Name, cn) - i++ - - } + if len(sel.DistinctOn) != 0 { + c.renderDistinctOn(sel, ti) } - for _, ob := range sel.OrderBy { - if _, ok := colmap[ob.Col]; ok { - continue - } - colmap[ob.Col] = struct{}{} - - if i != 0 { - io.WriteString(c.w, `, `) - } - colWithTable(c.w, ti.Name, ob.Col) - i++ - } - - for _, col := range childCols { - if _, ok := colmap[col.Name]; ok { - continue - } - if i != 0 { - io.WriteString(c.w, `, `) - } - - //fmt.Fprintf(w, `"%s"."%s"`, col.Table, col.Name) - colWithTable(c.w, col.Table, col.Name) - i++ + realColsRendered, isAgg, err := c.renderBaseColumns(sel, ti, childCols, skipped) + if err != nil { + return err } io.WriteString(c.w, ` FROM `) c.renderFrom(sel, ti, rel) - // if tn, ok := c.tmap[sel.Name]; ok { - // //fmt.Fprintf(w, ` FROM "%s" AS "%s"`, tn, c.sel.Name) - // tableWithAlias(c.w, ti.Name, sel.Name) - // } else { - // //fmt.Fprintf(w, ` FROM "%s"`, c.sel.Name) - // io.WriteString(c.w, `"`) - // io.WriteString(c.w, sel.Name) - // io.WriteString(c.w, `"`) - // } - if isRoot && isFil { io.WriteString(c.w, ` WHERE (`) if err := c.renderWhere(sel, ti); err != nil { @@ -741,17 +536,20 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, r io.WriteString(c.w, `)`) } - if isAgg { - if len(groupBy) != 0 { - io.WriteString(c.w, ` GROUP BY `) + if isAgg && len(realColsRendered) != 0 { + io.WriteString(c.w, ` GROUP BY `) - for i, id := range groupBy { - if i != 0 { - io.WriteString(c.w, `, `) - } - //fmt.Fprintf(w, `"%s"."%s"`, c.sel.Name, c.sel.Cols[id].Name) - colWithTable(c.w, ti.Name, sel.Cols[id].Name) - } + for i, id := range realColsRendered { + c.renderComma(i) + //fmt.Fprintf(w, `"%s"."%s"`, c.sel.Name, c.sel.Cols[id].Name) + colWithTable(c.w, ti.Name, sel.Cols[id].Name) + } + } + + if hasOrder { + err := c.renderOrderBy(sel, ti) + if err != nil { + return err } } @@ -779,10 +577,6 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, r io.WriteString(c.w, `') :: integer`) } - //fmt.Fprintf(w, `) AS "%s_%d"`, c.sel.Name, c.sel.ID) - io.WriteString(c.w, `)`) - aliasWithID(c.w, ti.Name, sel.ID) - return nil } @@ -824,23 +618,6 @@ func (c *compilerContext) renderFrom(sel *qcode.Select, ti *DBTableInfo, rel *DB return nil } -func (c *compilerContext) renderOrderByColumns(sel *qcode.Select, ti *DBTableInfo) { - //colsRendered := len(sel.Cols) != 0 - - for i := range sel.OrderBy { - //io.WriteString(w, ", ") - io.WriteString(c.w, `, `) - - col := sel.OrderBy[i].Col - //fmt.Fprintf(w, `"%s_%d"."%s" AS "%s_%d_%s_ob"`, - //c.sel.Name, c.sel.ID, c, - //c.sel.Name, c.sel.ID, c) - colWithTableID(c.w, ti.Name, sel.ID, col) - io.WriteString(c.w, ` AS `) - tableIDColSuffix(c.w, sel.Name, sel.ID, col, "_ob") - } -} - func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo) error { parent := c.s[sel.ParentID] @@ -961,7 +738,6 @@ func (c *compilerContext) renderExp(ex *qcode.Exp, ti *DBTableInfo, skipNested b switch val.Op { case qcode.OpFalse: st.Push(val.Op) - qcode.FreeExp(val) case qcode.OpAnd, qcode.OpOr: st.Push(')') @@ -972,12 +748,12 @@ func (c *compilerContext) renderExp(ex *qcode.Exp, ti *DBTableInfo, skipNested b } } st.Push('(') - qcode.FreeExp(val) case qcode.OpNot: + //fmt.Printf("1> %s %d %s %s\n", val.Op, len(val.Children), val.Children[0].Op, val.Children[1].Op) + st.Push(val.Children[0]) st.Push(qcode.OpNot) - qcode.FreeExp(val) default: if !skipNested && len(val.NestedCols) != 0 { @@ -992,14 +768,13 @@ func (c *compilerContext) renderExp(ex *qcode.Exp, ti *DBTableInfo, skipNested b if err := c.renderOp(val, ti); err != nil { return err } - qcode.FreeExp(val) } } + //qcode.FreeExp(val) default: return fmt.Errorf("12: unexpected value %v (%t)", intf, intf) } - } return nil @@ -1161,31 +936,20 @@ func (c *compilerContext) renderOrderBy(sel *qcode.Select, ti *DBTableInfo) erro io.WriteString(c.w, `, `) } ob := sel.OrderBy[i] + colWithTable(c.w, ti.Name, ob.Col) switch ob.Order { case qcode.OrderAsc: - //fmt.Fprintf(w, `"%s_%d.ob.%s" ASC`, sel.Name, sel.ID, ob.Col) - tableIDColSuffix(c.w, sel.Name, sel.ID, ob.Col, "_ob") io.WriteString(c.w, ` ASC`) case qcode.OrderDesc: - //fmt.Fprintf(w, `"%s_%d.ob.%s" DESC`, sel.Name, sel.ID, ob.Col) - tableIDColSuffix(c.w, sel.Name, sel.ID, ob.Col, "_ob") io.WriteString(c.w, ` DESC`) case qcode.OrderAscNullsFirst: - //fmt.Fprintf(w, `"%s_%d.ob.%s" ASC NULLS FIRST`, sel.Name, sel.ID, ob.Col) - tableIDColSuffix(c.w, sel.Name, sel.ID, ob.Col, "_ob") io.WriteString(c.w, ` ASC NULLS FIRST`) case qcode.OrderDescNullsFirst: - //fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS FIRST`, sel.Name, sel.ID, ob.Col) - tableIDColSuffix(c.w, sel.Name, sel.ID, ob.Col, "_ob") io.WriteString(c.w, ` DESC NULLLS FIRST`) case qcode.OrderAscNullsLast: - //fmt.Fprintf(w, `"%s_%d.ob.%s ASC NULLS LAST`, sel.Name, sel.ID, ob.Col) - tableIDColSuffix(c.w, sel.Name, sel.ID, ob.Col, "_ob") io.WriteString(c.w, ` ASC NULLS LAST`) case qcode.OrderDescNullsLast: - //fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS LAST`, sel.Name, sel.ID, ob.Col) - tableIDColSuffix(c.w, sel.Name, sel.ID, ob.Col, "_ob") io.WriteString(c.w, ` DESC NULLS LAST`) default: return fmt.Errorf("13: unexpected value %v", ob.Order) @@ -1200,8 +964,7 @@ func (c *compilerContext) renderDistinctOn(sel *qcode.Select, ti *DBTableInfo) { if i != 0 { io.WriteString(c.w, `, `) } - //fmt.Fprintf(w, `"%s_%d.ob.%s"`, c.sel.Name, c.sel.ID, c.sel.DistinctOn[i]) - tableIDColSuffix(c.w, ti.Name, sel.ID, sel.DistinctOn[i], "_ob") + colWithTable(c.w, ti.Name, sel.DistinctOn[i]) } io.WriteString(c.w, `) `) } @@ -1225,35 +988,22 @@ func (c *compilerContext) renderList(ex *qcode.Exp) { } func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string, col *DBColumn) { - io.WriteString(c.w, ` `) + io.WriteString(c.w, ` '`) - switch ex.Type { - case qcode.ValBool, qcode.ValInt, qcode.ValFloat: - if len(ex.Val) != 0 { - io.WriteString(c.w, ex.Val) - } else { - io.WriteString(c.w, `''`) - } - - case qcode.ValStr: - io.WriteString(c.w, `'`) - io.WriteString(c.w, ex.Val) - io.WriteString(c.w, `'`) - - case qcode.ValVar: - io.WriteString(c.w, `'`) + if ex.Type == qcode.ValVar { if val, ok := vars[ex.Val]; ok { io.WriteString(c.w, val) } else { - //fmt.Fprintf(w, `'{{%s}}'`, ex.Val) io.WriteString(c.w, `{{`) io.WriteString(c.w, ex.Val) io.WriteString(c.w, `}}`) } - io.WriteString(c.w, `' :: `) - io.WriteString(c.w, col.Type) + } else { + io.WriteString(c.w, ex.Val) } - //io.WriteString(c.w, `)`) + + io.WriteString(c.w, `' :: `) + io.WriteString(c.w, col.Type) } func funcPrefixLen(fn string) int { @@ -1303,15 +1053,6 @@ func aliasWithID(w io.Writer, alias string, id int32) { io.WriteString(w, `"`) } -func aliasWithIDSuffix(w io.Writer, alias string, id int32, suffix string) { - io.WriteString(w, ` AS "`) - io.WriteString(w, alias) - io.WriteString(w, `_`) - int2string(w, id) - io.WriteString(w, suffix) - io.WriteString(w, `"`) -} - func colWithTable(w io.Writer, table, col string) { io.WriteString(w, `"`) io.WriteString(w, table) @@ -1332,27 +1073,16 @@ func colWithTableID(w io.Writer, table string, id int32, col string) { io.WriteString(w, `"`) } -func colWithTableIDAlias(w io.Writer, table string, id int32, col, alias string) { +func quoted(w io.Writer, identifier string) { io.WriteString(w, `"`) - io.WriteString(w, table) - io.WriteString(w, `_`) - int2string(w, id) - io.WriteString(w, `"."`) - io.WriteString(w, col) - io.WriteString(w, `" AS "`) - io.WriteString(w, alias) + io.WriteString(w, identifier) io.WriteString(w, `"`) } -func tableIDColSuffix(w io.Writer, table string, id int32, col, suffix string) { - io.WriteString(w, `"`) - io.WriteString(w, table) - io.WriteString(w, `_`) - int2string(w, id) - io.WriteString(w, `_`) - io.WriteString(w, col) - io.WriteString(w, suffix) - io.WriteString(w, `"`) +func squoted(w io.Writer, identifier string) { + io.WriteString(w, `'`) + io.WriteString(w, identifier) + io.WriteString(w, `'`) } const charset = "0123456789" diff --git a/psql/query_test.go b/psql/query_test.go index b441a95..b0875e7 100644 --- a/psql/query_test.go +++ b/psql/query_test.go @@ -10,16 +10,16 @@ func withComplexArgs(t *testing.T) { proDUcts( # returns only 30 items limit: 30, - + # starts from item 10, commented out for now # offset: 10, - + # orders the response items by highest price order_by: { price: desc }, - + # no duplicate prices returned distinct: [ price ] - + # only items with an id >= 20 and < 28 are returned where: { id: { and: { greater_or_equals: 20, lt: 28 } } }) { id @@ -28,16 +28,41 @@ func withComplexArgs(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0" ORDER BY "products_0_price_ob" DESC), '[]') AS "json_0" FROM (SELECT DISTINCT ON ("products_0_price_ob") row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "json_row_0")) AS "json_0", "products_0"."price" AS "products_0_price_ob" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."id") < 28) AND (("products"."id") >= 20)))) LIMIT ('30') :: integer) AS "products_0" ORDER BY "products_0_price_ob" DESC LIMIT ('30') :: integer) AS "json_agg_0") AS "sel_0"` + compileGQLToPSQL(t, gql, nil, "user") +} - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } +func withWhereAndList(t *testing.T) { + gql := `query { + products( + where: { + and: [ + { not: { id: { is_null: true } } }, + { price: { gt: 10 } }, + ] } ) { + id + name + price + } + }` - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "user") +} + +func withWhereIsNull(t *testing.T) { + gql := `query { + products( + where: { + and: { + not: { id: { is_null: true } }, + price: { gt: 10 } + }}) { + id + name + price + } + }` + + compileGQLToPSQL(t, gql, nil, "user") } func withWhereMultiOr(t *testing.T) { @@ -56,68 +81,7 @@ func withWhereMultiOr(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "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"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") < 20) OR (("products"."price") > 10) OR NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } -} - -func withWhereIsNull(t *testing.T) { - gql := `query { - products( - where: { - and: { - not: { id: { is_null: true } }, - price: { gt: 10 } - }}) { - id - name - price - } - }` - - sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "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"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") > 10) AND NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } -} - -func withWhereAndList(t *testing.T) { - gql := `query { - products( - where: { - and: [ - { not: { id: { is_null: true } } }, - { price: { gt: 10 } }, - ] } ) { - id - name - price - } - }` - - sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "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"."price" AS "price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") > 10) AND NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "user") } func fetchByID(t *testing.T) { @@ -128,16 +92,7 @@ func fetchByID(t *testing.T) { } }` - sql := `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" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND (("products"."id") = 15))) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "user") } func searchQuery(t *testing.T) { @@ -150,16 +105,7 @@ func searchQuery(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "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"."search_rank" AS "search_rank", "products_0"."search_headline_description" AS "search_headline_description") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", ts_rank("products"."tsv", websearch_to_tsquery('ale')) AS "search_rank", ts_headline("products"."description", websearch_to_tsquery('ale')) AS "search_headline_description" FROM "products" WHERE ((("products"."tsv") @@ websearch_to_tsquery('ale'))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "admin") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "admin") } func oneToMany(t *testing.T) { @@ -173,16 +119,7 @@ func oneToMany(t *testing.T) { } }` - sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."email" AS "email", "products_1_join"."json_1" AS "products") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id")) AND ((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "user") } func oneToManyReverse(t *testing.T) { @@ -196,16 +133,7 @@ func oneToManyReverse(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."price" AS "price", "users_1_join"."json_1" AS "users") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name", "products"."price", "products"."user_id" FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('20') :: integer) AS "users_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "users_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "user") } func oneToManyArray(t *testing.T) { @@ -227,16 +155,7 @@ func oneToManyArray(t *testing.T) { } }` - sql := `SELECT row_to_json("json_root") FROM (SELECT "sel_0"."json_0" AS "tags", "sel_2"."json_2" AS "product" FROM (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "products_2"."name" AS "name", "products_2"."price" AS "price", "tags_3_join"."json_3" AS "tags") AS "json_row_2")) AS "json_2" FROM (SELECT "products"."name", "products"."price", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_3"), '[]') AS "json_3" FROM (SELECT row_to_json((SELECT "json_row_3" FROM (SELECT "tags_3"."id" AS "id", "tags_3"."name" AS "name") AS "json_row_3")) AS "json_3" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_2"."tags"))) LIMIT ('20') :: integer) AS "tags_3" LIMIT ('20') :: integer) AS "json_agg_3") AS "tags_3_join" ON ('true') LIMIT ('1') :: integer) AS "sel_2", (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "tags_0"."name" AS "name", "product_1_join"."json_1" AS "product") AS "json_row_0")) AS "json_0" FROM (SELECT "tags"."name", "tags"."slug" FROM "tags" LIMIT ('20') :: integer) AS "tags_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name" FROM "products" WHERE ((("tags_0"."slug") = any ("products"."tags"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0") AS "json_root"` - - resSQL, err := compileGQLToPSQL(gql, nil, "admin") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "admin") } func manyToMany(t *testing.T) { @@ -250,16 +169,7 @@ func manyToMany(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name", "customers_1_join"."json_1" AS "customers") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "customers_1"."email" AS "email", "customers_1"."full_name" AS "full_name") AS "json_row_1")) AS "json_1" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_0"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "customers_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "user") } func manyToManyReverse(t *testing.T) { @@ -273,16 +183,7 @@ func manyToManyReverse(t *testing.T) { } }` - sql := `SELECT json_object_agg('customers', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "products_1_join"."json_1" AS "products") AS "json_row_0")) AS "json_0" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_1"), '[]') AS "json_1" FROM (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."name" AS "name") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id")) AND ((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "user") } func aggFunction(t *testing.T) { @@ -293,16 +194,7 @@ func aggFunction(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."count_price" AS "count_price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name", count("products"."price") AS "count_price" FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8))) GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "user") } func aggFunctionBlockedByCol(t *testing.T) { @@ -313,16 +205,7 @@ func aggFunctionBlockedByCol(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "anon") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "anon") } func aggFunctionDisabled(t *testing.T) { @@ -333,16 +216,7 @@ func aggFunctionDisabled(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "anon1") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "anon1") } func aggFunctionWithFilter(t *testing.T) { @@ -353,16 +227,7 @@ func aggFunctionWithFilter(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."max_price" AS "max_price") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", max("products"."price") AS "max_price" FROM "products" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND (("products"."id") > 10))) GROUP BY "products"."id" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "user") } func syntheticTables(t *testing.T) { @@ -372,16 +237,7 @@ func syntheticTables(t *testing.T) { } }` - sql := `SELECT json_object_agg('me', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT ) AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") IS NOT DISTINCT FROM '{{user_id}}' :: bigint)) LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "user") } func queryWithVariables(t *testing.T) { @@ -392,16 +248,7 @@ func queryWithVariables(t *testing.T) { } }` - sql := `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" WHERE ((((("products"."price") > 0) AND (("products"."price") < 8)) AND ((("products"."price") IS NOT DISTINCT FROM '{{product_price}}' :: numeric(7,2)) AND (("products"."id") = '{{product_id}}' :: bigint)))) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "user") } func withWhereOnRelations(t *testing.T) { @@ -418,16 +265,7 @@ func withWhereOnRelations(t *testing.T) { } }` - sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "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" WHERE (NOT EXISTS (SELECT 1 FROM products WHERE (("products"."user_id") = ("users"."id")) AND ((("products"."price") > 3)))) LIMIT ('20') :: integer) AS "users_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "user") } func multiRoot(t *testing.T) { @@ -451,16 +289,7 @@ func multiRoot(t *testing.T) { } }` - sql := `SELECT row_to_json("json_root") FROM (SELECT "sel_0"."json_0" AS "customer", "sel_1"."json_1" AS "user", "sel_2"."json_2" AS "product" FROM (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "products_2"."id" AS "id", "products_2"."name" AS "name", "customers_3_join"."json_3" AS "customers", "customer_4_join"."json_4" AS "customer") AS "json_row_2")) AS "json_2" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") > 0) AND (("products"."price") < 8))) LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_4" FROM (SELECT "customers_4"."email" AS "email") AS "json_row_4")) AS "json_4" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('1') :: integer) AS "customers_4" LIMIT ('1') :: integer) AS "customer_4_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("json_3"), '[]') AS "json_3" FROM (SELECT row_to_json((SELECT "json_row_3" FROM (SELECT "customers_3"."email" AS "email") AS "json_row_3")) AS "json_3" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_3" LIMIT ('20') :: integer) AS "json_agg_3") AS "customers_3_join" ON ('true') LIMIT ('1') :: integer) AS "sel_2", (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "sel_1", (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "customers_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "customers"."id" FROM "customers" LIMIT ('1') :: integer) AS "customers_0" LIMIT ('1') :: integer) AS "sel_0") AS "json_root"` - - resSQL, err := compileGQLToPSQL(gql, nil, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "user") } func jsonColumnAsTable(t *testing.T) { @@ -477,16 +306,7 @@ func jsonColumnAsTable(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "tag_count_1_join"."json_1" AS "tag_count") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "tag_count_1"."count" AS "count", "tags_2_join"."json_2" AS "tags") AS "json_row_1")) AS "json_1" FROM (SELECT "tag_count"."count", "tag_count"."tag_id" FROM "products", json_to_recordset("products"."tag_count") AS "tag_count"(tag_id bigint, count int) WHERE ((("products"."id") = ("products_0"."id"))) LIMIT ('1') :: integer) AS "tag_count_1" 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"."name" AS "name") AS "json_row_2")) AS "json_2" FROM (SELECT "tags"."name" FROM "tags" WHERE ((("tags"."id") = ("tag_count_1"."tag_id"))) LIMIT ('20') :: integer) AS "tags_2" LIMIT ('20') :: integer) AS "json_agg_2") AS "tags_2_join" ON ('true') LIMIT ('1') :: integer) AS "tag_count_1_join" ON ('true') LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "admin") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "admin") } func skipUserIDForAnonRole(t *testing.T) { @@ -501,16 +321,7 @@ func skipUserIDForAnonRole(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "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", "products"."user_id" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "anon") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "anon") } func blockedQuery(t *testing.T) { @@ -522,16 +333,7 @@ func blockedQuery(t *testing.T) { } }` - sql := `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") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE (false) LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "bad_dude") } func blockedFunctions(t *testing.T) { @@ -542,16 +344,7 @@ func blockedFunctions(t *testing.T) { } }` - sql := `SELECT json_object_agg('users', json_0) FROM (SELECT coalesce(json_agg("json_0"), '[]') AS "json_0" FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."email" FROM "users" WHERE (false) LIMIT ('20') :: integer) AS "users_0" LIMIT ('20') :: integer) AS "json_agg_0") AS "sel_0"` - - resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, nil, "bad_dude") } func TestCompileQuery(t *testing.T) { diff --git a/psql/tables.go b/psql/tables.go index 0eeaccc..c5959ef 100644 --- a/psql/tables.go +++ b/psql/tables.go @@ -244,3 +244,13 @@ ORDER BY id;` return cols, nil } + +// func GetValType(type string) qcode.ValType { +// switch { +// case "bigint", "integer", "smallint", "numeric", "bigserial": +// return qcode.ValInt +// case "double precision", "real": +// return qcode.ValFloat +// case "" +// } +// } diff --git a/psql/tests.sql b/psql/tests.sql new file mode 100644 index 0000000..6bb1310 --- /dev/null +++ b/psql/tests.sql @@ -0,0 +1,148 @@ +=== RUN TestCompileInsert +=== RUN TestCompileInsert/simpleInsert +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_build_object('user', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id") AS "json" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0") AS "sel_0" +=== RUN TestCompileInsert/singleInsert +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_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileInsert/bulkInsert +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_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileInsert/simpleInsertWithPresets +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_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id") AS "json" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileInsert/nestedInsertManyToMany +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_build_object('purchase', "sel_0"."json") as "__root" FROM (SELECT json_build_object('sale_type', "purchases_0"."sale_type", 'quantity', "purchases_0"."quantity", 'due_date', "purchases_0"."due_date", 'product', "sel_1"."json", 'customer', "sel_2"."json") AS "json" 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 json_build_object('id', "customers_2"."id", 'full_name', "customers_2"."full_name", 'email', "customers_2"."email") AS "json" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "sel_1" ON ('true')) AS "sel_0" +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_build_object('purchase', "sel_0"."json") as "__root" FROM (SELECT json_build_object('sale_type', "purchases_0"."sale_type", 'quantity', "purchases_0"."quantity", 'due_date', "purchases_0"."due_date", 'product', "sel_1"."json", 'customer', "sel_2"."json") AS "json" 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 json_build_object('id', "customers_2"."id", 'full_name', "customers_2"."full_name", 'email', "customers_2"."email") AS "json" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileInsert/nestedInsertOneToMany +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_build_object('user', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email", 'product', "sel_1"."json") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileInsert/nestedInsertOneToOne +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_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileInsert/nestedInsertOneToManyWithConnect +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_build_object('user', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email", 'product', "sel_1"."json") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileInsert/nestedInsertOneToOneWithConnect +WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_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", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "sel_1"."json", 'tags', "sel_2"."json") AS "json" 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_build_object('id', "tags_2"."id", 'name', "tags_2"."name")), '[]') AS "json" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_0"."tags"))) LIMIT ('20') :: integer) AS "tags_2") AS "sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileInsert/nestedInsertOneToOneWithConnectArray +WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_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", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "sel_1" ON ('true')) AS "sel_0" +--- PASS: TestCompileInsert (0.02s) + --- PASS: TestCompileInsert/simpleInsert (0.00s) + --- PASS: TestCompileInsert/singleInsert (0.00s) + --- PASS: TestCompileInsert/bulkInsert (0.00s) + --- PASS: TestCompileInsert/simpleInsertWithPresets (0.00s) + --- PASS: TestCompileInsert/nestedInsertManyToMany (0.00s) + --- PASS: TestCompileInsert/nestedInsertOneToMany (0.00s) + --- PASS: TestCompileInsert/nestedInsertOneToOne (0.00s) + --- PASS: TestCompileInsert/nestedInsertOneToManyWithConnect (0.00s) + --- PASS: TestCompileInsert/nestedInsertOneToOneWithConnect (0.00s) + --- PASS: TestCompileInsert/nestedInsertOneToOneWithConnectArray (0.00s) +=== RUN TestCompileMutate +=== RUN TestCompileMutate/singleUpsert +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_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileMutate/singleUpsertWhere +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' :: numeric(7,2)) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileMutate/bulkUpsert +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_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileMutate/delete +WITH "products" AS (DELETE FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") IS NOT DISTINCT FROM '1' :: bigint)) RETURNING "products".*)SELECT json_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "sel_0" +--- PASS: TestCompileMutate (0.00s) + --- PASS: TestCompileMutate/singleUpsert (0.00s) + --- PASS: TestCompileMutate/singleUpsertWhere (0.00s) + --- PASS: TestCompileMutate/bulkUpsert (0.00s) + --- PASS: TestCompileMutate/delete (0.00s) +=== RUN TestCompileQuery +=== RUN TestCompileQuery/withComplexArgs +SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'price', "products_0"."price")), '[]') AS "json" FROM (SELECT DISTINCT ON ("products"."price") "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND ((("products"."id") < '28' :: bigint) AND (("products"."id") >= '20' :: bigint)))) ORDER BY "products"."price" DESC LIMIT ('30') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileQuery/withWhereAndList +SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'price', "products_0"."price")), '[]') AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND ((("products"."price") > '10' :: numeric(7,2)) AND NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileQuery/withWhereIsNull +SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'price', "products_0"."price")), '[]') AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND ((("products"."price") > '10' :: numeric(7,2)) AND NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileQuery/withWhereMultiOr +SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'price', "products_0"."price")), '[]') AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND ((("products"."price") < '20' :: numeric(7,2)) OR (("products"."price") > '10' :: numeric(7,2)) OR NOT (("products"."id") IS NULL)))) LIMIT ('20') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileQuery/fetchByID +SELECT json_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") = '15' :: bigint))) LIMIT ('1') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileQuery/searchQuery +SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'search_rank', "products_0"."search_rank", 'search_headline_description', "products_0"."search_headline_description")), '[]') AS "json" FROM (SELECT "products"."id", "products"."name", ts_rank("products"."tsv", websearch_to_tsquery('ale')) AS "search_rank", ts_headline("products"."description", websearch_to_tsquery('ale')) AS "search_headline_description" FROM "products" WHERE ((("products"."tsv") @@ websearch_to_tsquery('ale'))) LIMIT ('20') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileQuery/oneToMany +SELECT json_build_object('users', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('email', "users_0"."email", 'products', "sel_1"."json")), '[]') AS "json" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg(json_build_object('name', "products_1"."name", 'price', "products_1"."price")), '[]') AS "json" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id")) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileQuery/oneToManyReverse +SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('name', "products_0"."name", 'price', "products_0"."price", 'users', "sel_1"."json")), '[]') AS "json" FROM (SELECT "products"."name", "products"."price", "products"."user_id" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg(json_build_object('email', "users_1"."email")), '[]') AS "json" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('20') :: integer) AS "users_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileQuery/oneToManyArray +SELECT json_build_object('tags', "sel_0"."json", 'product', "sel_2"."json") as "__root" FROM (SELECT json_build_object('name', "products_2"."name", 'price', "products_2"."price", 'tags', "sel_3"."json") AS "json" FROM (SELECT "products"."name", "products"."price", "products"."tags" FROM "products" LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg(json_build_object('id', "tags_3"."id", 'name', "tags_3"."name")), '[]') AS "json" FROM (SELECT "tags"."id", "tags"."name" FROM "tags" WHERE ((("tags"."slug") = any ("products_2"."tags"))) LIMIT ('20') :: integer) AS "tags_3") AS "sel_3" ON ('true')) AS "sel_2", (SELECT coalesce(json_agg(json_build_object('name', "tags_0"."name", 'product', "sel_1"."json")), '[]') AS "json" FROM (SELECT "tags"."name", "tags"."slug" FROM "tags" LIMIT ('20') :: integer) AS "tags_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('name', "products_1"."name") AS "json" FROM (SELECT "products"."name" FROM "products" WHERE ((("tags_0"."slug") = any ("products"."tags"))) LIMIT ('1') :: integer) AS "products_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileQuery/manyToMany +SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('name', "products_0"."name", 'customers', "sel_1"."json")), '[]') AS "json" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg(json_build_object('email', "customers_1"."email", 'full_name', "customers_1"."full_name")), '[]') AS "json" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_0"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileQuery/manyToManyReverse +SELECT json_build_object('customers', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('email', "customers_0"."email", 'full_name', "customers_0"."full_name", 'products', "sel_1"."json")), '[]') AS "json" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg(json_build_object('name', "products_1"."name")), '[]') AS "json" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id")) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('20') :: integer) AS "products_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileQuery/aggFunction +SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('name', "products_0"."name", 'count_price', "products_0"."count_price")), '[]') AS "json" FROM (SELECT "products"."name", price("products"."price") AS "count_price" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileQuery/aggFunctionBlockedByCol +SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('name', "products_0"."name")), '[]') AS "json" FROM (SELECT "products"."name" FROM "products" GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileQuery/aggFunctionDisabled +SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('name', "products_0"."name")), '[]') AS "json" FROM (SELECT "products"."name" FROM "products" GROUP BY "products"."name" LIMIT ('20') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileQuery/aggFunctionWithFilter +SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('id', "products_0"."id", 'max_price', "products_0"."max_price")), '[]') AS "json" FROM (SELECT "products"."id", pri("products"."price") AS "max_price" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") > '10' :: bigint))) GROUP BY "products"."id" LIMIT ('20') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileQuery/syntheticTables +SELECT json_build_object('me', "sel_0"."json") as "__root" FROM (SELECT json_build_object() AS "json" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") IS NOT DISTINCT FROM '{{user_id}}' :: bigint)) LIMIT ('1') :: integer) AS "users_0") AS "sel_0" +=== RUN TestCompileQuery/queryWithVariables +SELECT json_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND ((("products"."price") IS NOT DISTINCT FROM '{{product_price}}' :: numeric(7,2)) AND (("products"."id") = '{{product_id}}' :: bigint)))) LIMIT ('1') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileQuery/withWhereOnRelations +SELECT json_build_object('users', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('id', "users_0"."id", 'email', "users_0"."email")), '[]') AS "json" FROM (SELECT "users"."id", "users"."email" FROM "users" WHERE (NOT EXISTS (SELECT 1 FROM products WHERE (("products"."user_id") = ("users"."id")) AND ((("products"."price") > '3' :: numeric(7,2))))) LIMIT ('20') :: integer) AS "users_0") AS "sel_0" +=== RUN TestCompileQuery/multiRoot +SELECT json_build_object('customer', "sel_0"."json", 'user', "sel_1"."json", 'product', "sel_2"."json") as "__root" FROM (SELECT json_build_object('id', "products_2"."id", 'name', "products_2"."name", 'customers', "sel_3"."json", 'customer', "sel_4"."json") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2)))) LIMIT ('1') :: integer) AS "products_2" LEFT OUTER JOIN LATERAL (SELECT json_build_object('email', "customers_4"."email") AS "json" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('1') :: integer) AS "customers_4") AS "sel_4" ON ('true') LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg(json_build_object('email', "customers_3"."email")), '[]') AS "json" FROM (SELECT "customers"."email" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_2"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_3") AS "sel_3" ON ('true')) AS "sel_2", (SELECT json_build_object('id', "users_1"."id", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_1") AS "sel_1", (SELECT json_build_object('id', "customers_0"."id") AS "json" FROM (SELECT "customers"."id" FROM "customers" LIMIT ('1') :: integer) AS "customers_0") AS "sel_0" +=== RUN TestCompileQuery/jsonColumnAsTable +SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'tag_count', "sel_1"."json")), '[]') AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('count', "tag_count_1"."count", 'tags', "sel_2"."json") AS "json" FROM (SELECT "tag_count"."count", "tag_count"."tag_id" FROM "products", json_to_recordset("products"."tag_count") AS "tag_count"(tag_id bigint, count int) WHERE ((("products"."id") = ("products_0"."id"))) LIMIT ('1') :: integer) AS "tag_count_1" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg(json_build_object('name', "tags_2"."name")), '[]') AS "json" FROM (SELECT "tags"."name" FROM "tags" WHERE ((("tags"."id") = ("tag_count_1"."tag_id"))) LIMIT ('20') :: integer) AS "tags_2") AS "sel_2" ON ('true')) AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileQuery/skipUserIDForAnonRole +SELECT json_build_object('products', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('id', "products_0"."id", 'name', "products_0"."name")), '[]') AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('20') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileQuery/blockedQuery +SELECT json_build_object('user', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE (false) LIMIT ('1') :: integer) AS "users_0") AS "sel_0" +=== RUN TestCompileQuery/blockedFunctions +SELECT json_build_object('users', "sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg(json_build_object('email', "users_0"."email")), '[]') AS "json" FROM (SELECT , "users"."email" FROM "users" WHERE (false) GROUP BY "users"."email" LIMIT ('20') :: integer) AS "users_0") AS "sel_0" +--- PASS: TestCompileQuery (0.02s) + --- PASS: TestCompileQuery/withComplexArgs (0.00s) + --- PASS: TestCompileQuery/withWhereAndList (0.00s) + --- PASS: TestCompileQuery/withWhereIsNull (0.00s) + --- PASS: TestCompileQuery/withWhereMultiOr (0.00s) + --- PASS: TestCompileQuery/fetchByID (0.00s) + --- PASS: TestCompileQuery/searchQuery (0.00s) + --- PASS: TestCompileQuery/oneToMany (0.00s) + --- PASS: TestCompileQuery/oneToManyReverse (0.00s) + --- PASS: TestCompileQuery/oneToManyArray (0.00s) + --- PASS: TestCompileQuery/manyToMany (0.00s) + --- PASS: TestCompileQuery/manyToManyReverse (0.00s) + --- PASS: TestCompileQuery/aggFunction (0.00s) + --- PASS: TestCompileQuery/aggFunctionBlockedByCol (0.00s) + --- PASS: TestCompileQuery/aggFunctionDisabled (0.00s) + --- PASS: TestCompileQuery/aggFunctionWithFilter (0.00s) + --- PASS: TestCompileQuery/syntheticTables (0.00s) + --- PASS: TestCompileQuery/queryWithVariables (0.00s) + --- PASS: TestCompileQuery/withWhereOnRelations (0.00s) + --- PASS: TestCompileQuery/multiRoot (0.00s) + --- PASS: TestCompileQuery/jsonColumnAsTable (0.00s) + --- PASS: TestCompileQuery/skipUserIDForAnonRole (0.00s) + --- PASS: TestCompileQuery/blockedQuery (0.00s) + --- PASS: TestCompileQuery/blockedFunctions (0.00s) +=== RUN TestCompileUpdate +=== RUN TestCompileUpdate/singleUpdate +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") IS NOT DISTINCT FROM '1' :: bigint) AND (("products"."id") = '15' :: bigint)) RETURNING "products".*) SELECT json_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name") AS "json" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileUpdate/simpleUpdateWithPresets +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") IS NOT DISTINCT FROM '{{user_id}}' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id") AS "json" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "sel_0" +=== RUN TestCompileUpdate/nestedUpdateManyToMany +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' :: bigint) RETURNING "purchases".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*) SELECT json_build_object('purchase', "sel_0"."json") as "__root" FROM (SELECT json_build_object('sale_type', "purchases_0"."sale_type", 'quantity', "purchases_0"."quantity", 'due_date', "purchases_0"."due_date", 'product', "sel_1"."json", 'customer', "sel_2"."json") AS "json" 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 json_build_object('id', "customers_2"."id", 'full_name', "customers_2"."full_name", 'email', "customers_2"."email") AS "json" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "sel_1" ON ('true')) AS "sel_0" +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' :: bigint) RETURNING "purchases".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*) SELECT json_build_object('purchase', "sel_0"."json") as "__root" FROM (SELECT json_build_object('sale_type', "purchases_0"."sale_type", 'quantity', "purchases_0"."quantity", 'due_date', "purchases_0"."due_date", 'product', "sel_1"."json", 'customer', "sel_2"."json") AS "json" 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 json_build_object('id', "customers_2"."id", 'full_name', "customers_2"."full_name", 'email', "customers_2"."email") AS "json" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2") AS "sel_2" ON ('true') LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileUpdate/nestedUpdateOneToMany +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' :: bigint) 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_build_object('user', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email", 'product', "sel_1"."json") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileUpdate/nestedUpdateOneToOne +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' :: bigint) RETURNING "products".*), "users" AS (UPDATE "users" SET ("email") = (SELECT "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j->'user') t) FROM "products" WHERE (("users"."id") = ("products"."user_id")) RETURNING "users".*) SELECT json_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileUpdate/nestedUpdateOneToManyWithConnect +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' :: bigint) 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_build_object('user', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "users_0"."id", 'full_name', "users_0"."full_name", 'email', "users_0"."email", 'product', "sel_1"."json") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "products_1"."id", 'name', "products_1"."name", 'price', "products_1"."price") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('1') :: integer) AS "products_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileUpdate/nestedUpdateOneToOneWithConnect +WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_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", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = '9' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "sel_1" ON ('true')) AS "sel_0" +WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_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", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = '9' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user', "sel_1"."json") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT json_build_object('id', "users_1"."id", 'full_name', "users_1"."full_name", 'email', "users_1"."email") AS "json" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1") AS "sel_1" ON ('true')) AS "sel_0" +=== RUN TestCompileUpdate/nestedUpdateOneToOneWithDisconnect +WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT * FROM (VALUES(NULL::bigint)) AS LOOKUP("id")), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = '2' :: bigint) RETURNING "products".*) SELECT json_build_object('product', "sel_0"."json") as "__root" FROM (SELECT json_build_object('id', "products_0"."id", 'name', "products_0"."name", 'user_id', "products_0"."user_id") AS "json" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0") AS "sel_0" +--- PASS: TestCompileUpdate (0.02s) + --- PASS: TestCompileUpdate/singleUpdate (0.00s) + --- PASS: TestCompileUpdate/simpleUpdateWithPresets (0.00s) + --- PASS: TestCompileUpdate/nestedUpdateManyToMany (0.00s) + --- PASS: TestCompileUpdate/nestedUpdateOneToMany (0.00s) + --- PASS: TestCompileUpdate/nestedUpdateOneToOne (0.00s) + --- PASS: TestCompileUpdate/nestedUpdateOneToManyWithConnect (0.00s) + --- PASS: TestCompileUpdate/nestedUpdateOneToOneWithConnect (0.00s) + --- PASS: TestCompileUpdate/nestedUpdateOneToOneWithDisconnect (0.00s) +PASS +ok github.com/dosco/super-graph/psql 0.127s diff --git a/psql/update_test.go b/psql/update_test.go index 0734f78..b1afdf0 100644 --- a/psql/update_test.go +++ b/psql/update_test.go @@ -13,20 +13,11 @@ func singleUpdate(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{update}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "description") = (SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE ((("products"."id") IS NOT DISTINCT FROM 1) AND (("products"."id") = 15)) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` - vars := map[string]json.RawMessage{ "update": json.RawMessage(` { "name": "my_name", "description": "my_desc" }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "anon") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, vars, "anon") } func simpleUpdateWithPresets(t *testing.T) { @@ -36,20 +27,11 @@ func simpleUpdateWithPresets(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."user_id") IS NOT DISTINCT FROM '{{user_id}}' :: bigint) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` - vars := map[string]json.RawMessage{ "data": json.RawMessage(`{"name": "Apple", "price": 1.25}`), } - resSQL, err := compileGQLToPSQL(gql, vars, "user") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, vars, "user") } func nestedUpdateManyToMany(t *testing.T) { @@ -71,10 +53,6 @@ func nestedUpdateManyToMany(t *testing.T) { } }` - sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t) WHERE (("purchases"."id") = 5) RETURNING "purchases".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` - - sql2 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "purchases" AS (UPDATE "purchases" SET ("sale_type", "quantity", "due_date") = (SELECT "t"."sale_type", "t"."quantity", "t"."due_date" FROM "_sg_input" i, json_populate_record(NULL::purchases, i.j) t) WHERE (("purchases"."id") = 5) RETURNING "purchases".*), "customers" AS (UPDATE "customers" SET ("full_name", "email") = (SELECT "t"."full_name", "t"."email" FROM "_sg_input" i, json_populate_record(NULL::customers, i.j->'customer') t) FROM "purchases" WHERE (("customers"."id") = ("purchases"."customer_id")) RETURNING "customers".*), "products" AS (UPDATE "products" SET ("name", "price") = (SELECT "t"."name", "t"."price" FROM "_sg_input" i, json_populate_record(NULL::products, i.j->'product') t) FROM "purchases" WHERE (("products"."id") = ("purchases"."product_id")) RETURNING "products".*) SELECT json_object_agg('purchase', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "purchases_0"."sale_type" AS "sale_type", "purchases_0"."quantity" AS "quantity", "purchases_0"."due_date" AS "due_date", "product_1_join"."json_1" AS "product", "customer_2_join"."json_2" AS "customer") AS "json_row_0")) AS "json_0" FROM (SELECT "purchases"."sale_type", "purchases"."quantity", "purchases"."due_date", "purchases"."product_id", "purchases"."customer_id" FROM "purchases" LIMIT ('1') :: integer) AS "purchases_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_2" FROM (SELECT "customers_2"."id" AS "id", "customers_2"."full_name" AS "full_name", "customers_2"."email" AS "email") AS "json_row_2")) AS "json_2" FROM (SELECT "customers"."id", "customers"."full_name", "customers"."email" FROM "customers" WHERE ((("customers"."id") = ("purchases_0"."customer_id"))) LIMIT ('1') :: integer) AS "customers_2" LIMIT ('1') :: integer) AS "customer_2_join" ON ('true') LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "products_1"."id" AS "id", "products_1"."name" AS "name", "products_1"."price" AS "price") AS "json_row_1")) AS "json_1" FROM (SELECT "products"."id", "products"."name", "products"."price" FROM "products" WHERE ((("products"."id") = ("purchases_0"."product_id"))) LIMIT ('1') :: integer) AS "products_1" LIMIT ('1') :: integer) AS "product_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` - vars := map[string]json.RawMessage{ "data": json.RawMessage(` { "sale_type": "bought", @@ -92,17 +70,7 @@ func nestedUpdateManyToMany(t *testing.T) { `), } - for i := 0; i < 1000; i++ { - resSQL, err := compileGQLToPSQL(gql, vars, "admin") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql1 && string(resSQL) != sql2 { - t.Fatal(errNotExpected) - } - } - + compileGQLToPSQL(t, gql, vars, "admin") } func nestedUpdateOneToMany(t *testing.T) { @@ -119,8 +87,6 @@ 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"= ((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(`{ "email": "thedude@rug.com", @@ -139,14 +105,7 @@ func nestedUpdateOneToMany(t *testing.T) { }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "admin") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, vars, "admin") } func nestedUpdateOneToOne(t *testing.T) { @@ -162,8 +121,6 @@ func nestedUpdateOneToOne(t *testing.T) { } }` - sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "created_at", "updated_at") = (SELECT "t"."name", "t"."price", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 6) RETURNING "products".*), "users" AS (UPDATE "users" SET ("email") = (SELECT "t"."email" FROM "_sg_input" i, json_populate_record(NULL::users, i.j->'user') t) FROM "products" WHERE (("users"."id") = ("products"."user_id")) RETURNING "users".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` - vars := map[string]json.RawMessage{ "data": json.RawMessage(`{ "name": "Apple", @@ -176,14 +133,8 @@ func nestedUpdateOneToOne(t *testing.T) { }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "admin") - if err != nil { - t.Fatal(err) - } + compileGQLToPSQL(t, gql, vars, "admin") - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } } func nestedUpdateOneToManyWithConnect(t *testing.T) { @@ -200,8 +151,6 @@ 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"= ((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(`{ "email": "thedude@rug.com", @@ -215,14 +164,7 @@ func nestedUpdateOneToManyWithConnect(t *testing.T) { }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "admin") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql1 { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, vars, "admin") } func nestedUpdateOneToOneWithConnect(t *testing.T) { @@ -238,10 +180,6 @@ func nestedUpdateOneToOneWithConnect(t *testing.T) { } }` - sql1 := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_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", "_x_users"."id" FROM "_sg_input" i, "_x_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), "_x_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", "_x_users"."id" FROM "_sg_input" i, "_x_users", json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 9) RETURNING "products".*) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "user_1_join"."json_1" AS "user") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name", "products"."user_id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT row_to_json((SELECT "json_row_1" FROM (SELECT "users_1"."id" AS "id", "users_1"."full_name" AS "full_name", "users_1"."email" AS "email") AS "json_row_1")) AS "json_1" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('1') :: integer) AS "users_1" LIMIT ('1') :: integer) AS "user_1_join" ON ('true') LIMIT ('1') :: integer) AS "sel_0"` - vars := map[string]json.RawMessage{ "data": json.RawMessage(`{ "name": "Apple", @@ -252,16 +190,7 @@ func nestedUpdateOneToOneWithConnect(t *testing.T) { }`), } - for i := 0; i < 1000; i++ { - resSQL, err := compileGQLToPSQL(gql, vars, "admin") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql1 && string(resSQL) != sql2 { - t.Fatal(errNotExpected) - } - } + compileGQLToPSQL(t, gql, vars, "admin") } func nestedUpdateOneToOneWithDisconnect(t *testing.T) { @@ -272,9 +201,6 @@ func nestedUpdateOneToOneWithDisconnect(t *testing.T) { user_id } }` - - sql := `WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT * FROM (VALUES(NULL::bigint)) AS LOOKUP("id")), "products" AS (UPDATE "products" SET ("name", "price", "user_id") = (SELECT "t"."name", "t"."price", "_x_users"."id" FROM "_sg_input" i, "_x_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", @@ -285,14 +211,7 @@ func nestedUpdateOneToOneWithDisconnect(t *testing.T) { }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "admin") - if err != nil { - t.Fatal(err) - } - - if string(resSQL) != sql { - t.Fatal(errNotExpected) - } + compileGQLToPSQL(t, gql, vars, "admin") } // func nestedUpdateOneToOneWithDisconnectArray(t *testing.T) { diff --git a/qcode/parse.go b/qcode/parse.go index e4043d1..6c2ce92 100644 --- a/qcode/parse.go +++ b/qcode/parse.go @@ -556,6 +556,31 @@ func (t parserType) String() string { return fmt.Sprintf("<%s>", v) } -func FreeNode(n *Node) { +// type Frees struct { +// n *Node +// loc int +// } + +// var freeList []Frees + +// func FreeNode(n *Node, loc int) { +// j := -1 + +// for i := range freeList { +// if n == freeList[i].n { +// j = i +// break +// } +// } + +// if j == -1 { +// nodePool.Put(n) +// freeList = append(freeList, Frees{n, loc}) +// } else { +// fmt.Printf(">>>>(%d) RE_FREE %d %p %s %s\n", loc, freeList[j].loc, freeList[j].n, n.Name, n.Type) +// } +// } + +func FreeNode(n *Node, loc int) { nodePool.Put(n) } diff --git a/qcode/qcode.go b/qcode/qcode.go index 3d68c17..c0a3c94 100644 --- a/qcode/qcode.go +++ b/qcode/qcode.go @@ -84,9 +84,19 @@ type OrderBy struct { Order Order } +type PagingType int + +const ( + PtOffset PagingType = iota + PtForward + PtBackward +) + type Paging struct { + Type PagingType Limit string Offset string + Cursor string NoLimit bool } @@ -183,6 +193,14 @@ func NewCompiler(c Config) (*Compiler, error) { return co, nil } +func AddFilter(sel *Select) *Exp { + ex := expPool.Get().(*Exp) + ex.Reset() + addFilter(sel, ex) + + return ex +} + func (com *Compiler) AddRole(role, table string, trc TRConfig) error { var err error trv := &trval{} @@ -400,10 +418,6 @@ func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) { } else if role == "anon" { // Tables not defined under the anon role will not be rendered sel.SkipRender = true - return - - } else { - return } if fil == nil { @@ -418,55 +432,58 @@ func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) { case OpNop: case OpFalse: sel.Where = fil - default: - if sel.Where != nil { - ow := sel.Where - - sel.Where = expPool.Get().(*Exp) - sel.Where.Reset() - sel.Where.Op = OpAnd - sel.Where.Children = sel.Where.childrenA[:2] - sel.Where.Children[0] = fil - sel.Where.Children[1] = ow - } else { - sel.Where = fil - } + addFilter(sel, fil) } } func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg, role string) error { var err error - var ka bool + + // don't free this arg either previously done or will be free'd + // in the future like in psql + var df bool for i := range args { arg := &args[i] switch arg.Name { case "id": - err, ka = com.compileArgID(sel, arg) + err, df = com.compileArgID(sel, arg) case "search": - err, ka = com.compileArgSearch(sel, arg) + err, df = com.compileArgSearch(sel, arg) case "where": - err, ka = com.compileArgWhere(sel, arg, role) + err, df = com.compileArgWhere(sel, arg, role) case "orderby", "order_by", "order": - err, ka = com.compileArgOrderBy(sel, arg) + err, df = com.compileArgOrderBy(sel, arg) case "distinct_on", "distinct": - err, ka = com.compileArgDistinctOn(sel, arg) + err, df = com.compileArgDistinctOn(sel, arg) case "limit": - err, ka = com.compileArgLimit(sel, arg) + err, df = com.compileArgLimit(sel, arg) case "offset": - err, ka = com.compileArgOffset(sel, arg) + err, df = com.compileArgOffset(sel, arg) + + case "first": + err, df = com.compileArgFirstLast(sel, arg, PtForward) + + case "last": + err, df = com.compileArgFirstLast(sel, arg, PtBackward) + + case "after": + err, df = com.compileArgAfterBefore(sel, arg, PtForward) + + case "before": + err, df = com.compileArgAfterBefore(sel, arg, PtBackward) } - if !ka { - nodePool.Put(arg.Val) + if !df { + FreeNode(arg.Val, 5) } if err != nil { @@ -529,7 +546,7 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (* var needsUser bool if node == nil || len(node.Children) == 0 { - return nil, needsUser, errors.New("invalid argument value") + return nil, false, errors.New("invalid argument value") } pushChild(st, nil, node) @@ -540,6 +557,7 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (* } intf := st.Pop() + node, ok := intf.(*Node) if !ok || node == nil { return nil, needsUser, fmt.Errorf("16: unexpected value %v (%t)", intf, intf) @@ -576,19 +594,23 @@ func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (* } } - pushChild(st, nil, node) + if usePool { + st.Push(node) - for { - if st.Len() == 0 { - break + for { + if st.Len() == 0 { + break + } + intf := st.Pop() + node, ok := intf.(*Node) + if !ok || node == nil { + continue + } + for i := range node.Children { + st.Push(node.Children[i]) + } + FreeNode(node, 1) } - intf := st.Pop() - node, _ := intf.(*Node) - - for i := range node.Children { - st.Push(node.Children[i]) - } - nodePool.Put(node) } return root, needsUser, nil @@ -644,19 +666,7 @@ func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) (error, bool) { } sel.Args[arg.Name] = arg.Val - - if sel.Where != nil { - ow := sel.Where - - sel.Where = expPool.Get().(*Exp) - sel.Where.Reset() - sel.Where.Op = OpAnd - sel.Where.Children = sel.Where.childrenA[:2] - sel.Where.Children[0] = ex - sel.Where.Children[1] = ow - } else { - sel.Where = ex - } + addFilter(sel, ex) return nil, true } @@ -672,21 +682,9 @@ func (com *Compiler) compileArgWhere(sel *Select, arg *Arg, role string) (error, if nu && role == "anon" { sel.SkipRender = true } + addFilter(sel, ex) - if sel.Where != nil { - ow := sel.Where - - sel.Where = expPool.Get().(*Exp) - sel.Where.Reset() - sel.Where.Op = OpAnd - sel.Where.Children = sel.Where.childrenA[:2] - sel.Where.Children[0] = ex - sel.Where.Children[1] = ow - } else { - sel.Where = ex - } - - return nil, false + return nil, true } func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) { @@ -713,7 +711,7 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) { } if _, ok := com.bl[node.Name]; ok { - nodePool.Put(node) + //FreeNode(node, 2) continue } @@ -721,7 +719,7 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) { for i := range node.Children { st.Push(node.Children[i]) } - nodePool.Put(node) + //FreeNode(node, 3) continue } @@ -746,7 +744,7 @@ func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) { setOrderByColName(ob, node) sel.OrderBy = append(sel.OrderBy, ob) - nodePool.Put(node) + //FreeNode(node, 4) } return nil, false } @@ -768,8 +766,9 @@ func (com *Compiler) compileArgDistinctOn(sel *Select, arg *Arg) (error, bool) { for i := range node.Children { sel.DistinctOn = append(sel.DistinctOn, node.Children[i].Val) - nodePool.Put(node.Children[i]) + FreeNode(node.Children[i], 5) } + //FreeNode(node, 5) return nil, false } @@ -797,6 +796,32 @@ func (com *Compiler) compileArgOffset(sel *Select, arg *Arg) (error, bool) { return nil, false } +func (com *Compiler) compileArgFirstLast(sel *Select, arg *Arg, pt PagingType) (error, bool) { + node := arg.Val + + if node.Type != NodeInt { + return fmt.Errorf("expecting an integer"), false + } + + sel.Paging.Type = pt + sel.Paging.Limit = node.Val + + return nil, false +} + +func (com *Compiler) compileArgAfterBefore(sel *Select, arg *Arg, pt PagingType) (error, bool) { + node := arg.Val + + if node.Type != NodeStr { + return fmt.Errorf("expecting a string"), false + } + + sel.Paging.Type = pt + sel.Paging.Cursor = node.Val + + return nil, false +} + var zeroTrv = &trval{} func (com *Compiler) getRole(role, field string) *trval { @@ -807,6 +832,22 @@ func (com *Compiler) getRole(role, field string) *trval { } } +func addFilter(sel *Select, fil *Exp) { + if sel.Where != nil { + ow := sel.Where + + sel.Where = expPool.Get().(*Exp) + sel.Where.Reset() + + sel.Where.Op = OpAnd + sel.Where.Children = sel.Where.childrenA[:2] + sel.Where.Children[0] = fil + sel.Where.Children[1] = ow + } else { + sel.Where = fil + } +} + func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) { name := node.Name if name[0] == '_' { @@ -821,6 +862,7 @@ func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) { } else { ex = &Exp{doFree: false} } + ex.Children = ex.childrenA[:0] switch name { @@ -997,7 +1039,6 @@ func pushChildren(st *util.Stack, exp *Exp, node *Node) { func pushChild(st *util.Stack, exp *Exp, node *Node) { node.Children[0].exp = exp st.Push(node.Children[0]) - } func compileFilter(filter []string) (*Exp, bool, error) { diff --git a/serv/cmd.go b/serv/cmd.go index c8a0098..ba8f8cd 100644 --- a/serv/cmd.go +++ b/serv/cmd.go @@ -29,15 +29,17 @@ var ( ) var ( - logger zerolog.Logger // logger for everything but errors - errlog zerolog.Logger // logger for errors includes line numbers - conf *config // parsed config - confPath string // path to the config file - db *pgxpool.Pool // database connection pool - schema *psql.DBSchema // database tables, columns and relationships - allowList *allow.List // allow.list is contains queries allowed in production - qcompile *qcode.Compiler // qcode compiler - pcompile *psql.Compiler // postgres sql compiler + logger zerolog.Logger // logger for everything but errors + errlog zerolog.Logger // logger for errors includes line numbers + conf *config // parsed config + confPath string // path to the config file + db *pgxpool.Pool // database connection pool + schema *psql.DBSchema // database tables, columns and relationships + allowList *allow.List // allow.list is contains queries allowed in production + qcompile *qcode.Compiler // qcode compiler + pcompile *psql.Compiler // postgres sql compiler + secretKey [32]byte // encryption key + internalKey [32]byte // encryption key used for internal needs ) func Cmd() { diff --git a/serv/cmd_serv.go b/serv/cmd_serv.go index f1b3584..d254eea 100644 --- a/serv/cmd_serv.go +++ b/serv/cmd_serv.go @@ -19,6 +19,7 @@ func cmdServ(cmd *cobra.Command, args []string) { fatalInProd(err, "failed to connect to database") } + initCrypto() initCompiler() initResolvers() initAllowList(confPath) diff --git a/serv/config.go b/serv/config.go index 9662a7f..2c6a716 100644 --- a/serv/config.go +++ b/serv/config.go @@ -30,6 +30,7 @@ type config struct { AuthFailBlock bool `mapstructure:"auth_fail_block"` SeedFile string `mapstructure:"seed_file"` MigrationsPath string `mapstructure:"migrations_path"` + SecretKey string `mapstructure:"secret_key"` Inflections map[string]string diff --git a/serv/core.go b/serv/core.go index 2fdfe4b..147ce93 100644 --- a/serv/core.go +++ b/serv/core.go @@ -13,6 +13,7 @@ import ( "github.com/cespare/xxhash/v2" "github.com/dosco/super-graph/allow" "github.com/dosco/super-graph/qcode" + "github.com/jackc/pgx/v4" "github.com/valyala/fasttemplate" ) @@ -241,6 +242,10 @@ func (c *coreContext) resolveSQL() ([]byte, *stmt, error) { } } + if root, err = encryptCursor(st.qc, root); err != nil { + return nil, nil, err + } + if allowList.IsPersist() { if err := allowList.Set(c.req.Vars, c.req.Query, c.req.ref); err != nil { return nil, nil, err diff --git a/serv/cursor.go b/serv/cursor.go new file mode 100644 index 0000000..c2cb10e --- /dev/null +++ b/serv/cursor.go @@ -0,0 +1,67 @@ +package serv + +import ( + "bytes" + "encoding/base64" + + "github.com/dosco/super-graph/crypto" + "github.com/dosco/super-graph/jsn" + "github.com/dosco/super-graph/qcode" +) + +func encryptCursor(qc *qcode.QCode, data []byte) ([]byte, error) { + var keys [][]byte + + for _, s := range qc.Selects { + if s.Paging.Type != qcode.PtOffset { + var buf bytes.Buffer + + buf.WriteString(s.FieldName) + buf.WriteString("_cursor") + keys = append(keys, buf.Bytes()) + } + } + + if len(keys) == 0 { + return data, nil + } + + from := jsn.Get(data, keys) + to := make([]jsn.Field, len(from)) + + for i, f := range from { + to[i].Key = f.Key + + if f.Value[0] < '0' || f.Value[0] > '9' { + continue + } + + v, err := crypto.Encrypt(f.Value, &internalKey) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + buf.WriteByte('"') + buf.WriteString(base64.StdEncoding.EncodeToString(v)) + buf.WriteByte('"') + + to[i].Value = buf.Bytes() + } + + var buf bytes.Buffer + + if err := jsn.Replace(&buf, data, from, to); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func decrypt(data string) ([]byte, error) { + v, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return nil, err + } + return crypto.Decrypt(v, &internalKey) +} diff --git a/serv/init.go b/serv/init.go index 2b4aa39..956cdda 100644 --- a/serv/init.go +++ b/serv/init.go @@ -2,10 +2,12 @@ package serv import ( "context" + "crypto/sha256" "fmt" "os" "github.com/dosco/super-graph/allow" + "github.com/dosco/super-graph/crypto" "github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4/pgxpool" "github.com/rs/zerolog" @@ -163,3 +165,14 @@ func initAllowList(cpath string) { errlog.Fatal().Err(err).Msg("failed to initialize allow list") } } + +func initCrypto() { + if len(conf.SecretKey) != 0 { + secretKey = sha256.Sum256([]byte(conf.SecretKey)) + conf.SecretKey = "" + internalKey = secretKey + + } else { + internalKey = crypto.NewEncryptionKey() + } +} diff --git a/serv/serv.go b/serv/serv.go index 865ba63..d66d273 100644 --- a/serv/serv.go +++ b/serv/serv.go @@ -46,8 +46,9 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) { } pc := psql.NewCompiler(psql.Config{ - Schema: schema, - Vars: c.DB.Vars, + Schema: schema, + Decryptor: decrypt, + Vars: c.DB.Vars, }) return qc, pc, nil diff --git a/tmpl/dev.yml b/tmpl/dev.yml index f7b0bc5..68116e9 100644 --- a/tmpl/dev.yml +++ b/tmpl/dev.yml @@ -32,6 +32,10 @@ reload_on_config_change: true # Path pointing to where the migrations can be found migrations_path: ./config/migrations +# Secret key for general encryption operations like +# encrypting the cursor data +secret_key: supercalifajalistics + # Postgres related environment Variables # SG_DATABASE_HOST # SG_DATABASE_PORT diff --git a/tmpl/prod.yml b/tmpl/prod.yml index 802e907..5069c7d 100644 --- a/tmpl/prod.yml +++ b/tmpl/prod.yml @@ -32,6 +32,10 @@ enable_tracing: true # Path pointing to where the migrations can be found # migrations_path: migrations +# Secret key for general encryption operations like +# encrypting the cursor data +# secret_key: supercalifajalistics + # Postgres related environment Variables # SG_DATABASE_HOST # SG_DATABASE_PORT