diff --git a/psql/query.go b/psql/query.go index f95b11f..0011c1e 100644 --- a/psql/query.go +++ b/psql/query.go @@ -267,58 +267,29 @@ func (c *compilerContext) initSelect(sel *qcode.Select, ti *DBTableInfo, vars Va if sel.Paging.Type != qcode.PtOffset { colmap[ti.PrimaryCol.Key] = struct{}{} - - addToOrderBy := true + addPrimaryKey := true for _, ob := range sel.OrderBy { if ob.Col == ti.PrimaryCol.Key { - addToOrderBy = false - } - - if sel.Paging.Cursor { - fil := qcode.AddFilter(sel) - fil.Col = ob.Col - fil.Type = qcode.ValRef - fil.Table = "__cur" - fil.Val = ob.Col - - switch ob.Order { - case qcode.OrderAsc: - fil.Op = qcode.OpGreaterThan - - case qcode.OrderDesc: - fil.Op = qcode.OpLesserThan - } + addPrimaryKey = false + break } } - if addToOrderBy { - var op qcode.ExpOp + if addPrimaryKey { + ob := &qcode.OrderBy{Col: ti.PrimaryCol.Name, Order: qcode.OrderAsc} - ob := &qcode.OrderBy{Col: ti.PrimaryCol.Name} - sel.OrderBy = append(sel.OrderBy, ob) - - switch sel.Paging.Type { - case qcode.PtForward: - op = qcode.OpGreaterThan - ob.Order = qcode.OrderAsc - - case qcode.PtBackward: - op = qcode.OpLesserThan + if sel.Paging.Type == qcode.PtBackward { ob.Order = qcode.OrderDesc } - - if sel.Paging.Cursor { - fil := qcode.AddFilter(sel) - fil.Op = op - fil.Col = ti.PrimaryCol.Name - fil.Type = qcode.ValRef - fil.Table = "__cur" - fil.Val = ti.PrimaryCol.Name - } + sel.OrderBy = append(sel.OrderBy, ob) } } + if sel.Paging.Cursor { + c.addSeekPredicate(sel) + } + for _, id := range sel.Children { child := &c.s[id] @@ -364,6 +335,72 @@ func (c *compilerContext) initSelect(sel *qcode.Select, ti *DBTableInfo, vars Va return skipped, cols, nil } +// This +// (A, B, C) >= (X, Y, Z) +// +// Becomes +// (A > X) +// OR ((A = X) AND (B > Y)) +// OR ((A = X) AND (B = Y) AND (C > Z)) +// OR ((A = X) AND (B = Y) AND (C = Z)) + +func (c *compilerContext) addSeekPredicate(sel *qcode.Select) error { + var or, and *qcode.Exp + + obLen := len(sel.OrderBy) + + if obLen > 1 { + or = qcode.NewFilter() + or.Op = qcode.OpOr + } + + for i := 0; i < obLen; i++ { + if i > 0 { + and = qcode.NewFilter() + and.Op = qcode.OpAnd + } + + for n, ob := range sel.OrderBy { + f := qcode.NewFilter() + f.Col = ob.Col + f.Type = qcode.ValRef + f.Table = "__cur" + f.Val = ob.Col + + if obLen == 1 { + qcode.AddFilter(sel, f) + return nil + } + + switch { + case i > 0 && n != i: + f.Op = qcode.OpEquals + case ob.Order == qcode.OrderDesc: + f.Op = qcode.OpLesserThan + default: + f.Op = qcode.OpGreaterThan + } + + if and != nil { + and.Children = append(and.Children, f) + } else { + or.Children = append(or.Children, f) + } + + if n == i { + break + } + } + + if and != nil { + or.Children = append(or.Children, and) + } + } + + qcode.AddFilter(sel, or) + return nil +} + func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars Variables) (uint32, error) { var rel *DBRel var err error @@ -399,31 +436,6 @@ func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo, vars } } - //if !ti.Singular { - // 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 sel.Paging.Type != qcode.PtOffset { - // for i, ob := range sel.OrderBy { - // io.WriteString(c.w, `, LAST_VALUE(`) - // colWithTableID(c.w, ti.Name, sel.ID, ob.Col) - // io.WriteString(c.w, `) OVER() AS "__cur_`) - // int2string(c.w, int32(i)) - // io.WriteString(c.w, `"`) - // } - - // io.WriteString(c.w, `LAST_VALUE(`) - // colWithTableID(c.w, ti.Name, sel.ID, ti.PrimaryCol.Name) - // io.WriteString(c.w, `) OVER() AS "__cursor_`) - // int2string(c.w, int32(len(sel.OrderBy))) - // io.WriteString(c.w, `"`) - //} - - //} - io.WriteString(c.w, ` FROM (`) // FROM (SELECT .... ) @@ -959,8 +971,12 @@ func (c *compilerContext) renderOp(ex *qcode.Exp, ti *DBTableInfo) error { switch ex.Op { case qcode.OpEquals: - io.WriteString(c.w, `IS NOT DISTINCT FROM`) + io.WriteString(c.w, `=`) case qcode.OpNotEquals: + io.WriteString(c.w, `!=`) + case qcode.OpNotDistinct: + io.WriteString(c.w, `IS NOT DISTINCT FROM`) + case qcode.OpDistinct: io.WriteString(c.w, `IS DISTINCT FROM`) case qcode.OpGreaterOrEquals: io.WriteString(c.w, `>=`) diff --git a/psql/tests.sql b/psql/tests.sql index 57cace9..9e42e79 100644 --- a/psql/tests.sql +++ b/psql/tests.sql @@ -39,7 +39,7 @@ WITH "_sg_input" AS (SELECT '{{upsert}}' :: json AS j), "products" AS (INSERT IN === 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" +WITH "products" AS (DELETE FROM "products" WHERE (((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))) AND (("products"."id") = '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.01s) --- PASS: TestCompileMutate/singleUpsert (0.00s) --- PASS: TestCompileMutate/singleUpsertWhere (0.00s) @@ -77,9 +77,9 @@ SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT === RUN TestCompileQuery/aggFunctionWithFilter SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT 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") 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" +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") = '{{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") IS NOT DISTINCT FROM '{{product_price}}' :: numeric(7,2)) AND (("products"."id") = '{{product_id}}' :: bigint) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) LIMIT ('1') :: integer) AS "products_0") AS "__sel_0" +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") = '{{product_price}}' :: numeric(7,2)) AND (("products"."id") = '{{product_id}}' :: bigint) AND ((("products"."price") > '0' :: numeric(7,2)) AND (("products"."price") < '8' :: numeric(7,2))))) 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("__sel_0"."json"), '[]') as "json" FROM (SELECT 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") AS "__sel_0" === RUN TestCompileQuery/multiRoot @@ -87,7 +87,7 @@ SELECT json_build_object('customer', "__sel_0"."json", 'user', "__sel_1"."json", === RUN TestCompileQuery/jsonColumnAsTable SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT 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("__sel_2"."json"), '[]') as "json" FROM (SELECT 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") AS "__sel_2" ON ('true')) AS "__sel_1" ON ('true')) AS "__sel_0") AS "__sel_0" === RUN TestCompileQuery/withCursor -SELECT json_build_object('products', "__sel_0"."json", 'products_cursor', "__sel_0"."cursor") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json", CONCAT_WS(',', max("__cur_0"), max("__cur_1")) as "cursor" FROM (SELECT json_build_object('name', "products_0"."name") AS "json", LAST_VALUE("products_0"."price") OVER() AS "__cur_0", LAST_VALUE("products_0"."id") OVER() AS "__cur_1" FROM (WITH "__cur" AS (SELECT a[1] as "price", a[2] as "id" FROM string_to_array('{{cursor}}', ',') as a) SELECT "products"."name", "products"."id", "products"."price" FROM "products", "__cur" WHERE (((("products"."id") > "__cur"."id" :: bigint) AND (("products"."price") < "__cur"."price" :: numeric(7,2)))) ORDER BY "products"."price" DESC, "products"."id" ASC LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0" +SELECT json_build_object('products', "__sel_0"."json", 'products_cursor', "__sel_0"."cursor") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json", CONCAT_WS(',', max("__cur_0"), max("__cur_1")) as "cursor" FROM (SELECT json_build_object('name', "products_0"."name") AS "json", LAST_VALUE("products_0"."price") OVER() AS "__cur_0", LAST_VALUE("products_0"."id") OVER() AS "__cur_1" FROM (WITH "__cur" AS (SELECT a[1] as "price", a[2] as "id" FROM string_to_array('{{cursor}}', ',') as a) SELECT "products"."name", "products"."id", "products"."price" FROM "products", "__cur" WHERE (((("products"."price") < "__cur"."price" :: numeric(7,2)) OR ((("products"."price") = "__cur"."price" :: numeric(7,2)) AND (("products"."id") > "__cur"."id" :: bigint)))) ORDER BY "products"."price" DESC, "products"."id" ASC LIMIT ('20') :: integer) AS "products_0") AS "__sel_0") AS "__sel_0" === RUN TestCompileQuery/skipUserIDForAnonRole SELECT json_build_object('products', "__sel_0"."json") as "__root" FROM (SELECT coalesce(json_agg("__sel_0"."json"), '[]') as "json" FROM (SELECT 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") AS "__sel_0" === RUN TestCompileQuery/blockedQuery @@ -121,14 +121,14 @@ SELECT json_build_object('users', "__sel_0"."json") as "__root" FROM (SELECT coa --- 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") = '{{id}}' :: 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" +WITH "_sg_input" AS (SELECT '{{update}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "description") = (SELECT "t"."name", "t"."description" FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE ((("products"."id") = '1' :: bigint) AND (("products"."id") = '{{id}}' :: 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" +WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "products" AS (UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "t"."name", "t"."price", 'now' :: timestamp without time zone FROM "_sg_input" i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."user_id") = '{{user_id}}' :: bigint) RETURNING "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") = '{{id}}' :: 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") = '{{id}}' :: 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" +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") = '{{id}}' :: 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" === 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" +WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "users" AS (UPDATE "users" SET ("full_name", "email", "created_at", "updated_at") = (SELECT "t"."full_name", "t"."email", "t"."created_at", "t"."updated_at" FROM "_sg_input" i, json_populate_record(NULL::users, i.j) t) WHERE (("users"."id") = '8' :: 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") = '{{id}}' :: 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 @@ -148,4 +148,4 @@ WITH "_sg_input" AS (SELECT '{{data}}' :: json AS j), "_x_users" AS (SELECT * FR --- PASS: TestCompileUpdate/nestedUpdateOneToOneWithConnect (0.00s) --- PASS: TestCompileUpdate/nestedUpdateOneToOneWithDisconnect (0.00s) PASS -ok github.com/dosco/super-graph/psql 0.456s +ok github.com/dosco/super-graph/psql 0.716s diff --git a/qcode/qcode.go b/qcode/qcode.go index 0cee210..c217edf 100644 --- a/qcode/qcode.go +++ b/qcode/qcode.go @@ -131,6 +131,8 @@ const ( OpEqID OpTsQuery OpFalse + OpNotDistinct + OpDistinct ) type ValType int @@ -195,10 +197,9 @@ func NewCompiler(c Config) (*Compiler, error) { return co, nil } -func AddFilter(sel *Select) *Exp { +func NewFilter() *Exp { ex := expPool.Get().(*Exp) ex.Reset() - addFilter(sel, ex) return ex } @@ -363,8 +364,8 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error { return err } - // Order is important addFilters must come after compileArgs - com.addFilters(qc, s, role) + // Order is important AddFilters must come after compileArgs + com.AddFilters(qc, s, role) if s.ParentID == -1 { qc.Roots = append(qc.Roots, s.ID) @@ -410,7 +411,7 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error { return nil } -func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) { +func (com *Compiler) AddFilters(qc *QCode, sel *Select, role string) { var fil *Exp var nu bool @@ -435,7 +436,7 @@ func (com *Compiler) addFilters(qc *QCode, sel *Select, role string) { case OpFalse: sel.Where = fil default: - addFilter(sel, fil) + AddFilter(sel, fil) } } @@ -659,7 +660,7 @@ func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) (error, bool) { } sel.Args[arg.Name] = arg.Val - addFilter(sel, ex) + AddFilter(sel, ex) return nil, true } @@ -676,7 +677,7 @@ func (com *Compiler) compileArgWhere(sel *Select, arg *Arg, role string) (error, if nu && role == "anon" { sel.SkipRender = true } - addFilter(sel, ex) + AddFilter(sel, ex) return nil, true } @@ -820,7 +821,7 @@ func (com *Compiler) getRole(role, field string) *trval { } } -func addFilter(sel *Select, fil *Exp) { +func AddFilter(sel *Select, fil *Exp) { if sel.Where != nil { ow := sel.Where @@ -937,6 +938,12 @@ func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) { case "is_null": ex.Op = OpIsNull ex.Val = node.Val + case "null_eq", "ndis", "not_distinct": + ex.Op = OpNotDistinct + ex.Val = node.Val + case "null_neq", "dis", "distinct": + ex.Op = OpDistinct + ex.Val = node.Val default: pushChildren(st, node.exp, node) return nil, nil // skip node diff --git a/serv/cmd_serv.go b/serv/cmd_serv.go index d254eea..8377332 100644 --- a/serv/cmd_serv.go +++ b/serv/cmd_serv.go @@ -19,11 +19,13 @@ func cmdServ(cmd *cobra.Command, args []string) { fatalInProd(err, "failed to connect to database") } - initCrypto() - initCompiler() - initResolvers() - initAllowList(confPath) - initPreparedList(confPath) + if conf != nil && db != nil { + initCrypto() + initCompiler() + initResolvers() + initAllowList(confPath) + initPreparedList(confPath) + } startHTTP() } diff --git a/serv/cursor.go b/serv/cursor.go index 934d431..cf7f1d0 100644 --- a/serv/cursor.go +++ b/serv/cursor.go @@ -36,15 +36,20 @@ func encryptCursor(qc *qcode.QCode, data []byte) ([]byte, error) { continue } - v, err := crypto.Encrypt(f.Value[1:len(f.Value)-1], &internalKey) - if err != nil { - return nil, err - } - var buf bytes.Buffer - buf.WriteByte('"') - buf.WriteString(base64.StdEncoding.EncodeToString(v)) - buf.WriteByte('"') + + if len(f.Value) > 2 { + v, err := crypto.Encrypt(f.Value[1:len(f.Value)-1], &internalKey) + if err != nil { + return nil, err + } + + buf.WriteByte('"') + buf.WriteString(base64.StdEncoding.EncodeToString(v)) + buf.WriteByte('"') + } else { + buf.WriteString(`null`) + } to[i].Value = buf.Bytes() }