diff --git a/config/dev.yml b/config/dev.yml index 3bf2481..e0a7263 100644 --- a/config/dev.yml +++ b/config/dev.yml @@ -9,7 +9,7 @@ log_level: "debug" # from the allow list are permitted. # When it's 'false' all queries are saved to the # the allow list in ./config/allow.list -production: true +production: false # Throw a 401 on auth failure for queries that need auth auth_fail_block: false diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index f7f2667..436c885 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -26,7 +26,7 @@ module.exports = { ['meta', { prefix: ogprefix, property: 'og:title', content: title }], ['meta', { prefix: ogprefix, property: 'twitter:title', content: title }], ['meta', { prefix: ogprefix, property: 'og:type', content: 'website' }], - ['meta', { prefix: ogprefix, property: 'og:url', content: 'https://supergraph.dev }], + ['meta', { prefix: ogprefix, property: 'og:url', content: 'https://supergraph.dev' }], ['meta', { prefix: ogprefix, property: 'og:description', content: description }], //['meta', { prefix: ogprefix, property: 'og:image', content: 'https://wireupyourfrontend.com/assets/logo.png' }], // ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], diff --git a/docs/guide.md b/docs/guide.md index dc278f3..b816e4d 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -476,6 +476,21 @@ query { } ``` +Multiple tables can also be fetched using a single GraphQL query. This is very fast since the entire query is converted into a single SQL query which the database can efficiently run. + +```graphql +query { + user { + full_name + email + } + products { + name + description + } +} +``` + ### Fetching data To fetch a specific `product` by it's ID you can use the `id` argument. The real name id field will be resolved automatically so this query will work even if your id column is named something like `product_id`. @@ -908,6 +923,40 @@ class AddSearchColumn < ActiveRecord::Migration[5.1] end ``` +## GraphQL with React + +This is a quick simple example using `graphql.js` [https://github.com/f/graphql.js/](https://github.com/f/graphql.js/) + +```js +import React, { useState, useEffect } from 'react' +import graphql from 'graphql.js' + +// Create a GraphQL client pointing to Super Graph +var graph = graphql("http://localhost:3000/api/v1/graphql", { asJSON: true }) + +const App = () => { + const [user, setUser] = useState(null) + + useEffect(() => { + async function action() { + // Use the GraphQL client to execute a graphQL query + // The second argument to the client are the variables you need to pass + const result = await graph(`{ user { id first_name last_name picture_url } }`)() + setUser(result) + } + action() + }, []); + + return ( +
+

{ JSON.stringify(user) }

+
+ ); +} +``` + +export default App; + ## Authentication You can only have one type of auth enabled. You can either pick Rails or JWT. diff --git a/jsn/json_test.go b/jsn/json_test.go index 7b08a37..08fb1fe 100644 --- a/jsn/json_test.go +++ b/jsn/json_test.go @@ -374,10 +374,6 @@ func TestKeys2(t *testing.T) { "id", "posts", "title", "description", "full_name", "email", "books", "name", "description", } - // for i := range fields { - // fmt.Println("-->", string(fields[i])) - // } - if len(exp) != len(fields) { t.Errorf("Expected %d fields %d", len(exp), len(fields)) } diff --git a/psql/mutate_test.go b/psql/mutate_test.go index b719255..2c53b99 100644 --- a/psql/mutate_test.go +++ b/psql/mutate_test.go @@ -12,7 +12,7 @@ func simpleInsert(t *testing.T) { } }` - sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id" FROM "users") AS "users_0") AS "done_1337"` + sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), @@ -36,7 +36,7 @@ func singleInsert(t *testing.T) { } }` - sql := `WITH "products" AS (WITH "input" AS (SELECT {{insert}}::json AS j) INSERT INTO "products" ("name", "description", "user_id") SELECT "name", "description", "user_id" FROM input i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` + sql := `WITH "products" AS (WITH "input" AS (SELECT {{insert}}::json AS j) INSERT INTO "products" ("name", "description", "user_id") SELECT "name", "description", "user_id" FROM input i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "insert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc", "user_id": 5 }`), @@ -60,7 +60,7 @@ func bulkInsert(t *testing.T) { } }` - sql := `WITH "products" AS (WITH "input" AS (SELECT {{insert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_recordset(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` + sql := `WITH "products" AS (WITH "input" AS (SELECT {{insert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_recordset(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "insert": json.RawMessage(` [{ "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }]`), @@ -84,7 +84,7 @@ func singleUpsert(t *testing.T) { } }` - sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` + sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "upsert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), @@ -108,7 +108,7 @@ func singleUpsertWhere(t *testing.T) { } }` - sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t ON CONFLICT (id) WHERE (("products"."price") > 3) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` + sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t ON CONFLICT (id) WHERE (("products"."price") > 3) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "upsert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), @@ -132,7 +132,7 @@ func bulkUpsert(t *testing.T) { } }` - sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_recordset(NULL::products, i.j) t ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` + sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_recordset(NULL::products, i.j) t ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "upsert": json.RawMessage(` [{ "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }]`), @@ -156,7 +156,7 @@ func singleUpdate(t *testing.T) { } }` - sql := `WITH "products" AS (WITH "input" AS (SELECT {{update}}::json AS j) UPDATE "products" SET ("name", "description") = (SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 1) AND (("products"."id") = 15) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` + sql := `WITH "products" AS (WITH "input" AS (SELECT {{update}}::json AS j) UPDATE "products" SET ("name", "description") = (SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 1) AND (("products"."id") = 15) RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "update": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), @@ -180,7 +180,7 @@ func delete(t *testing.T) { } }` - sql := `WITH "products" AS (DELETE FROM "products" WHERE (("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."id") = 1) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` + sql := `WITH "products" AS (DELETE FROM "products" WHERE (("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."id") = 1) RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "update": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), @@ -203,7 +203,7 @@ func blockedInsert(t *testing.T) { } }` - sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t WHERE false RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id" FROM "users") AS "users_0") AS "done_1337"` + sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t WHERE false RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), @@ -227,7 +227,7 @@ func blockedUpdate(t *testing.T) { } }` - sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) UPDATE "users" SET ("full_name", "email") = (SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t) WHERE false RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id", "users"."email" FROM "users") AS "users_0") AS "done_1337"` + sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) UPDATE "users" SET ("full_name", "email") = (SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t) WHERE false RETURNING *) SELECT json_object_agg('user', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email") AS "json_row_0")) AS "json_0" FROM (SELECT "users"."id", "users"."email" FROM "users" LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), @@ -250,7 +250,7 @@ func simpleInsertWithPresets(t *testing.T) { } }` - sql := `WITH "products" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "name", "price", 'now' :: timestamp without time zone, 'now' :: timestamp without time zone, '$user_id' :: bigint FROM input i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id" FROM "products") AS "products_0") AS "done_1337"` + sql := `WITH "products" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "name", "price", 'now' :: timestamp without time zone, 'now' :: timestamp without time zone, '$user_id' :: bigint FROM input i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{"name": "Tomato", "price": 5.76}`), @@ -273,7 +273,7 @@ func simpleUpdateWithPresets(t *testing.T) { } }` - sql := `WITH "products" AS (WITH "input" AS (SELECT {{data}}::json AS j) UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "name", "price", 'now' :: timestamp without time zone FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."user_id") = {{user_id}}) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id" FROM "products") AS "products_0") AS "done_1337"` + sql := `WITH "products" AS (WITH "input" AS (SELECT {{data}}::json AS j) UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "name", "price", 'now' :: timestamp without time zone FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."user_id") = {{user_id}}) RETURNING *) SELECT json_object_agg('product', json_0) FROM (SELECT row_to_json((SELECT "json_row_0" FROM (SELECT "products_0"."id" AS "id") AS "json_row_0")) AS "json_0" FROM (SELECT "products"."id" FROM "products" LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{"name": "Apple", "price": 1.25}`), diff --git a/psql/query.go b/psql/query.go index 92fa009..772a03f 100644 --- a/psql/query.go +++ b/psql/query.go @@ -77,30 +77,49 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) { } c := &compilerContext{w, qc.Selects, co} - root := &qc.Selects[0] - - ti, err := c.schema.GetTable(root.Table) - if err != nil { - return 0, err - } + multiRoot := (len(qc.Roots) > 1) st := NewStack() - st.Push(root.ID + closeBlock) - st.Push(root.ID) - //fmt.Fprintf(w, `SELECT json_object_agg('%s', %s) FROM (`, - //root.FieldName, root.Table) - io.WriteString(c.w, `SELECT json_object_agg('`) - io.WriteString(c.w, root.FieldName) - io.WriteString(c.w, `', `) + if multiRoot { + io.WriteString(c.w, `SELECT row_to_json("json_root") FROM (SELECT `) + + for i, id := range qc.Roots { + root := qc.Selects[id] + + st.Push(root.ID + closeBlock) + st.Push(root.ID) + + if i != 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) + } + + io.WriteString(c.w, ` FROM `) - if ti.Singular == false { - io.WriteString(c.w, root.Table) } else { - io.WriteString(c.w, "sel_json_") + root := qc.Selects[0] + + io.WriteString(c.w, `SELECT json_object_agg(`) + io.WriteString(c.w, `'`) + io.WriteString(c.w, root.FieldName) + io.WriteString(c.w, `', `) + io.WriteString(c.w, `json_`) int2string(c.w, root.ID) + + st.Push(root.ID + closeBlock) + st.Push(root.ID) + + io.WriteString(c.w, `) FROM `) } - io.WriteString(c.w, `) FROM (`) var ignored uint32 @@ -114,16 +133,21 @@ 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.Table) if err != nil { return 0, err } - if sel.ID != 0 { + if sel.ParentID != -1 { if err = c.renderLateralJoin(sel); err != nil { return 0, err } } + skipped, err := c.renderSelect(sel, ti) if err != nil { return 0, err @@ -153,16 +177,25 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer) (uint32, error) { return 0, err } - if sel.ID != 0 { + if sel.ParentID != -1 { if err = c.renderLateralJoinClose(sel); err != nil { return 0, err } + } else { + io.WriteString(c.w, `)`) + aliasWithID(c.w, `sel`, sel.ID) + + if st.Len() != 0 { + io.WriteString(c.w, `, `) + } } + } } - io.WriteString(c.w, `)`) - alias(c.w, `done_1337`) + if multiRoot { + io.WriteString(c.w, `) AS "json_root"`) + } return ignored, nil } @@ -219,7 +252,7 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint if ti.Singular == false { //fmt.Fprintf(w, `SELECT coalesce(json_agg("%s"`, c.sel.Table) io.WriteString(c.w, `SELECT coalesce(json_agg("`) - io.WriteString(c.w, "sel_json_") + io.WriteString(c.w, "json_") int2string(c.w, sel.ID) io.WriteString(c.w, `"`) @@ -232,7 +265,7 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint //fmt.Fprintf(w, `), '[]') AS "%s" FROM (`, c.sel.Table) io.WriteString(c.w, `), '[]')`) - alias(c.w, sel.Table) + aliasWithID(c.w, "json", sel.ID) io.WriteString(c.w, ` FROM (`) } @@ -245,8 +278,8 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint io.WriteString(c.w, `row_to_json((`) - //fmt.Fprintf(w, `SELECT "sel_%d" FROM (SELECT `, c.sel.ID) - io.WriteString(c.w, `SELECT "sel_`) + //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 `) @@ -260,13 +293,13 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint return skipped, err } - //fmt.Fprintf(w, `) AS "sel_%d"`, c.sel.ID) + //fmt.Fprintf(w, `) AS "%d"`, c.sel.ID) io.WriteString(c.w, `)`) - aliasWithID(c.w, "sel", sel.ID) + aliasWithID(c.w, "json_row", sel.ID) //fmt.Fprintf(w, `)) AS "%s"`, c.sel.Table) io.WriteString(c.w, `))`) - aliasWithID(c.w, "sel_json", sel.ID) + aliasWithID(c.w, "json", sel.ID) // END-ROW-TO-JSON if hasOrder { @@ -295,8 +328,8 @@ func (c *compilerContext) renderSelectClose(sel *qcode.Select, ti *DBTableInfo) } switch { - case sel.Paging.NoLimit: - break + 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) @@ -304,8 +337,8 @@ func (c *compilerContext) renderSelectClose(sel *qcode.Select, ti *DBTableInfo) io.WriteString(c.w, sel.Paging.Limit) io.WriteString(c.w, `') :: integer`) - case ti.Singular: - io.WriteString(c.w, ` LIMIT ('1') :: integer`) + case sel.Paging.NoLimit: + break default: io.WriteString(c.w, ` LIMIT ('20') :: integer`) @@ -319,9 +352,9 @@ func (c *compilerContext) renderSelectClose(sel *qcode.Select, ti *DBTableInfo) } if ti.Singular == false { - //fmt.Fprintf(w, `) AS "sel_json_agg_%d"`, c.sel.ID) + //fmt.Fprintf(w, `) AS "json_agg_%d"`, c.sel.ID) io.WriteString(c.w, `)`) - aliasWithID(c.w, "sel_json_agg", sel.ID) + aliasWithID(c.w, "json_agg", sel.ID) } return nil @@ -445,24 +478,18 @@ func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo } childSel := &c.s[id] - cti, err := c.schema.GetTable(childSel.Table) - if err != nil { - return err - } - //fmt.Fprintf(w, `"%s_%d_join"."%s" AS "%s"`, //s.Table, s.ID, s.Table, s.FieldName) - if cti.Singular { - io.WriteString(c.w, `"sel_json_`) - int2string(c.w, childSel.ID) - io.WriteString(c.w, `" AS "`) - io.WriteString(c.w, childSel.FieldName) - io.WriteString(c.w, `"`) - - } else { - colWithTableIDSuffixAlias(c.w, childSel.Table, childSel.ID, - "_join", childSel.Table, childSel.FieldName) - } + //if cti.Singular { + io.WriteString(c.w, `"`) + io.WriteString(c.w, childSel.Table) + io.WriteString(c.w, `_`) + 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, `"`) } return nil @@ -472,7 +499,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, childCols []*qcode.Column, skipped uint32) error { var groupBy []int - isRoot := sel.ID == 0 + isRoot := sel.ParentID == -1 isFil := sel.Where != nil isSearch := sel.Args["search"] != nil isAgg := false @@ -641,8 +668,8 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, } switch { - case sel.Paging.NoLimit: - break + 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) @@ -650,8 +677,8 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo, io.WriteString(c.w, sel.Paging.Limit) io.WriteString(c.w, `') :: integer`) - case ti.Singular: - io.WriteString(c.w, ` LIMIT ('1') :: integer`) + case sel.Paging.NoLimit: + break default: io.WriteString(c.w, ` LIMIT ('20') :: integer`) diff --git a/psql/query_test.go b/psql/query_test.go index ebabeea..bd00060 100644 --- a/psql/query_test.go +++ b/psql/query_test.go @@ -28,7 +28,7 @@ func withComplexArgs(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0" ORDER BY "products_0_price_ob" DESC), '[]') AS "products" FROM (SELECT DISTINCT ON ("products_0_price_ob") row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "sel_0")) AS "sel_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 "sel_json_agg_0") AS "done_1337"` + 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"` resSQL, err := compileGQLToPSQL(gql, nil, "user") if err != nil { @@ -56,7 +56,7 @@ func withWhereMultiOr(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "sel_0")) AS "sel_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 "sel_json_agg_0") AS "done_1337"` + 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 { @@ -82,7 +82,7 @@ func withWhereIsNull(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "sel_0")) AS "sel_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 "sel_json_agg_0") AS "done_1337"` + 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 { @@ -108,7 +108,7 @@ func withWhereAndList(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name", "products_0"."price" AS "price") AS "sel_0")) AS "sel_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 "sel_json_agg_0") AS "done_1337"` + 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 { @@ -128,7 +128,7 @@ func fetchByID(t *testing.T) { } }` - sql := `SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_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 "done_1337"` + 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 { @@ -148,7 +148,7 @@ func searchQuery(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("tsv") @@ to_tsquery('Imperial'))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` + 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" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("tsv") @@ to_tsquery('Imperial'))) 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 { @@ -171,7 +171,7 @@ func oneToMany(t *testing.T) { } }` - sql := `SELECT json_object_agg('users', users) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "users" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."email" AS "email", "products_1_join"."products" AS "products") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."email", "users"."id" FROM "users" LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("sel_json_1"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "products_1"."name" AS "name", "products_1"."price" AS "price") AS "sel_1")) AS "sel_json_1" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "sel_json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` + 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"))) 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 { @@ -194,7 +194,7 @@ func belongsTo(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."price" AS "price", "users_1_join"."users" AS "users") AS "sel_0")) AS "sel_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("sel_json_1"), '[]') AS "users" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "users_1"."email" AS "email") AS "sel_1")) AS "sel_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 "sel_json_agg_1") AS "users_1_join" ON ('true') LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` + 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 { @@ -217,7 +217,7 @@ func manyToMany(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name", "customers_1_join"."customers" AS "customers") AS "sel_0")) AS "sel_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("sel_json_1"), '[]') AS "customers" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "customers_1"."email" AS "email", "customers_1"."full_name" AS "full_name") AS "sel_1")) AS "sel_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 "sel_json_agg_1") AS "customers_1_join" ON ('true') LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` + 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 { @@ -240,7 +240,7 @@ func manyToManyReverse(t *testing.T) { } }` - sql := `SELECT json_object_agg('customers', customers) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "customers" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "products_1_join"."products" AS "products") AS "sel_0")) AS "sel_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("sel_json_1"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "products_1"."name" AS "name") AS "sel_1")) AS "sel_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"))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "sel_json_agg_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` + 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"))) 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 { @@ -260,7 +260,7 @@ func aggFunction(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."count_price" AS "count_price") AS "sel_0")) AS "sel_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 "sel_json_agg_0") AS "done_1337"` + 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 { @@ -280,7 +280,7 @@ func aggFunctionBlockedByCol(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` + 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 { @@ -300,7 +300,7 @@ func aggFunctionDisabled(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."name" FROM "products" LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` + 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 { @@ -320,7 +320,7 @@ func aggFunctionWithFilter(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."max_price" AS "max_price") AS "sel_0")) AS "sel_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 "sel_json_agg_0") AS "done_1337"` + 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 { @@ -339,7 +339,7 @@ func syntheticTables(t *testing.T) { } }` - sql := `SELECT json_object_agg('me', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT ) AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = {{user_id}})) LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "done_1337"` + 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") = {{user_id}})) LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "sel_0"` resSQL, err := compileGQLToPSQL(gql, nil, "user") if err != nil { @@ -359,7 +359,7 @@ func queryWithVariables(t *testing.T) { } }` - sql := `SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((("products"."price") > 0) AND (("products"."price") < 8) AND (("products"."price") = {{product_price}}) AND (("products"."id") = {{product_id}})) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "done_1337"` + 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") = {{product_price}}) AND (("products"."id") = {{product_id}})) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "sel_0"` resSQL, err := compileGQLToPSQL(gql, nil, "user") if err != nil { @@ -385,7 +385,40 @@ func withWhereOnRelations(t *testing.T) { } }` - sql := `SELECT json_object_agg('users', users) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "users" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id", "users"."email" FROM "users" WHERE (NOT EXISTS (SELECT 1 FROM products WHERE (("products"."user_id") = ("users"."id")))) LIMIT ('20') :: integer) AS "users_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` + 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")))) 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) + } +} + +func multiRoot(t *testing.T) { + gql := `query { + product { + id + name + customer { + email + } + customers { + email + } + } + user { + id + email + } + customer { + id + } + }` + + 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 { @@ -406,7 +439,7 @@ func blockedQuery(t *testing.T) { } }` - sql := `SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email") AS "sel_0")) AS "sel_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 "done_1337"` + 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 { @@ -426,7 +459,7 @@ func blockedFunctions(t *testing.T) { } }` - sql := `SELECT json_object_agg('users', users) FROM (SELECT coalesce(json_agg("sel_json_0"), '[]') AS "users" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."email" AS "email") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."email" FROM "users" WHERE (false) LIMIT ('20') :: integer) AS "users_0" LIMIT ('20') :: integer) AS "sel_json_agg_0") AS "done_1337"` + 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 { @@ -456,6 +489,7 @@ func TestCompileQuery(t *testing.T) { t.Run("syntheticTables", syntheticTables) t.Run("queryWithVariables", queryWithVariables) t.Run("withWhereOnRelations", withWhereOnRelations) + t.Run("multiRoot", multiRoot) t.Run("blockedQuery", blockedQuery) t.Run("blockedFunctions", blockedFunctions) } diff --git a/qcode/parse.go b/qcode/parse.go index 0fe6c34..6600b79 100644 --- a/qcode/parse.go +++ b/qcode/parse.go @@ -148,24 +148,13 @@ func parseSelectionSet(gql []byte) (*Operation, error) { if p.peek(itemObjOpen) { p.ignore() - } - - if p.peek(itemName) { - op = opPool.Get().(*Operation) - op.Reset() - - op.Type = opQuery - op.Name = "" - op.Fields = op.fieldsA[:0] - op.Args = op.argsA[:0] - op.Fields, err = p.parseFields(op.Fields) - + op, err = p.parseQueryOp() } else { op, err = p.parseOp() + } - if err != nil { - return nil, err - } + if err != nil { + return nil, err } lexPool.Put(l) @@ -259,6 +248,37 @@ func (p *Parser) parseOp() (*Operation, error) { if p.peek(itemObjOpen) { p.ignore() + + for n := 0; n < 10; n++ { + if p.peek(itemName) == false { + break + } + + op.Fields, err = p.parseFields(op.Fields) + if err != nil { + return nil, err + } + } + } + + return op, nil +} + +func (p *Parser) parseQueryOp() (*Operation, error) { + op := opPool.Get().(*Operation) + op.Reset() + + op.Type = opQuery + op.Fields = op.fieldsA[:0] + op.Args = op.argsA[:0] + + var err error + + for n := 0; n < 10; n++ { + if p.peek(itemName) == false { + break + } + op.Fields, err = p.parseFields(op.Fields) if err != nil { return nil, err @@ -300,16 +320,12 @@ func (p *Parser) parseFields(fields []Field) ([]Field, error) { return nil, err } - if f.ID != 0 { - intf := st.Peek() - pid, ok := intf.(int32) - - if !ok { - return nil, fmt.Errorf("14: unexpected value %v (%t)", intf, intf) - } - + intf := st.Peek() + if pid, ok := intf.(int32); ok { f.ParentID = pid fields[pid].Children = append(fields[pid].Children, f.ID) + } else { + f.ParentID = -1 } if p.peek(itemObjOpen) { diff --git a/qcode/parse_test.go b/qcode/parse_test.go index ea74871..4335583 100644 --- a/qcode/parse_test.go +++ b/qcode/parse_test.go @@ -14,10 +14,10 @@ func TestCompile1(t *testing.T) { }) _, err := qc.Compile([]byte(` - product(id: 15) { + { product(id: 15) { id name - }`), "user") + } }`), "user") if err != nil { t.Fatal(err) diff --git a/qcode/qcode.go b/qcode/qcode.go index 6d8cf9f..571d46e 100644 --- a/qcode/qcode.go +++ b/qcode/qcode.go @@ -30,6 +30,8 @@ type QCode struct { Type QType ActionVar string Selects []Select + Roots []int32 + rootsA [5]int32 } type Select struct { @@ -233,6 +235,7 @@ func (com *Compiler) Compile(query []byte, role string) (*QCode, error) { var err error qc := QCode{Type: QTQuery} + qc.Roots = qc.rootsA[:0] op, err := Parse(query) if err != nil { @@ -250,7 +253,7 @@ func (com *Compiler) Compile(query []byte, role string) (*QCode, error) { func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error { id := int32(0) - parentID := int32(0) + parentID := int32(-1) if len(op.Fields) == 0 { return errors.New("invalid graphql no query found") @@ -269,7 +272,12 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error { if len(op.Fields) == 0 { return errors.New("empty query") } - st.Push(op.Fields[0].ID) + + for i := range op.Fields { + if op.Fields[i].ParentID == -1 { + st.Push(op.Fields[i].ID) + } + } for { if st.Len() == 0 { @@ -313,11 +321,6 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error { s.PresetList = trv.update.pslist } - if s.ID != 0 { - p := &selects[s.ParentID] - p.Children = append(p.Children, s.ID) - } - if len(field.Alias) != 0 { s.FieldName = field.Alias } else { @@ -329,6 +332,15 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error { return err } + // Order is important addFilters must come after compileArgs + if s.ParentID == -1 { + qc.Roots = append(qc.Roots, s.ID) + com.addFilters(qc, s, role) + } else { + p := &selects[s.ParentID] + p.Children = append(p.Children, s.ID) + } + s.Cols = make([]Column, 0, len(field.Children)) action = QTQuery @@ -362,36 +374,40 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error { return errors.New("invalid query") } - var fil *Exp - root := &selects[0] + qc.Selects = selects[:id] + return nil +} - if trv, ok := com.tr[role][op.Fields[0].Name]; ok { +func (com *Compiler) addFilters(qc *QCode, root *Select, role string) { + var fil *Exp + + if trv, ok := com.tr[role][root.Table]; ok { fil = trv.filter(qc.Type) } - if fil != nil { - switch fil.Op { - case OpNop: - case OpFalse: - root.Where = fil - default: - if root.Where != nil { - ow := root.Where - - root.Where = expPool.Get().(*Exp) - root.Where.Reset() - root.Where.Op = OpAnd - root.Where.Children = root.Where.childrenA[:2] - root.Where.Children[0] = fil - root.Where.Children[1] = ow - } else { - root.Where = fil - } - } + if fil == nil { + return } - qc.Selects = selects[:id] - return nil + switch fil.Op { + case OpNop: + case OpFalse: + root.Where = fil + + default: + if root.Where != nil { + ow := root.Where + + root.Where = expPool.Get().(*Exp) + root.Where.Reset() + root.Where.Op = OpAnd + root.Where.Children = root.Where.childrenA[:2] + root.Where.Children[0] = fil + root.Where.Children[1] = ow + } else { + root.Where = fil + } + } } func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg) error { diff --git a/serv/core.go b/serv/core.go index a0ffe12..fad4de1 100644 --- a/serv/core.go +++ b/serv/core.go @@ -52,8 +52,6 @@ func (c *coreContext) execQuery() ([]byte, error) { var qc *qcode.QCode var data []byte - logger.Debug().Str("role", c.req.role).Msg(c.req.Query) - if conf.Production { var ps *preparedItem @@ -156,12 +154,17 @@ func (c *coreContext) resolvePreparedSQL() ([]byte, *preparedItem, error) { if mutation { err = tx.QueryRow(c, ps.stmt.SQL, vars...).Scan(&root) } else { - err = tx.QueryRow(c, ps.stmt.SQL, vars...).Scan(&c.req.role, &root) + err = tx.QueryRow(c, ps.stmt.SQL, vars...).Scan(&role, &root) } + + logger.Debug().Str("default_role", c.req.role).Str("role", role).Msg(c.req.Query) + if err != nil { return nil, nil, err } + c.req.role = role + if err := tx.Commit(c); err != nil { return nil, nil, err } @@ -229,12 +232,23 @@ func (c *coreContext) resolveSQL() ([]byte, uint32, error) { } var root []byte + var role string + + log := logger.Debug() if mutation { err = tx.QueryRow(c, finalSQL).Scan(&root) + log = log.Str("role", role) + } else { - err = tx.QueryRow(c, finalSQL).Scan(&c.req.role, &root) + err = tx.QueryRow(c, finalSQL).Scan(&role, &root) + log = log.Str("default_role", c.req.role).Str("role", role) + c.req.role = role + } + + log.Msg(c.req.Query) + if err != nil { return nil, 0, err } @@ -250,10 +264,9 @@ func (c *coreContext) resolveSQL() ([]byte, uint32, error) { } if conf.EnableTracing && len(st.qc.Selects) != 0 { - c.addTrace( - st.qc.Selects, - st.qc.Selects[0].ID, - stime) + for _, id := range st.qc.Roots { + c.addTrace(st.qc.Selects, id, stime) + } } if conf.Production == false { @@ -451,7 +464,7 @@ func (c *coreContext) addTrace(sel []qcode.Select, id int32, st time.Time) { c.res.Extensions.Tracing.Duration = du n := 1 - for i := id; i != 0; i = sel[i].ParentID { + for i := id; i != -1; i = sel[i].ParentID { n++ } path := make([]string, n) @@ -459,7 +472,7 @@ func (c *coreContext) addTrace(sel []qcode.Select, id int32, st time.Time) { n-- for i := id; ; i = sel[i].ParentID { path[n] = sel[i].Table - if sel[i].ID == 0 { + if sel[i].ParentID == -1 { break } n-- diff --git a/serv/core_build.go b/serv/core_build.go index c08263d..79367a9 100644 --- a/serv/core_build.go +++ b/serv/core_build.go @@ -55,8 +55,11 @@ func (c *coreContext) buildStmt() ([]stmt, error) { } if conf.Production && role.Name == "anon" { - if _, ok := role.tablesMap[qc.Selects[0].Table]; !ok { - continue + for _, id := range qc.Roots { + root := qc.Selects[id] + if _, ok := role.tablesMap[root.Table]; !ok { + continue + } } }