From 940ad6d0909e8b9e81f4ea56f1cf071d8e569eb9 Mon Sep 17 00:00:00 2001 From: dosco <832235+dosco@users.noreply.github.com> Date: Thu, 27 Oct 2022 10:05:53 -0700 Subject: [PATCH] Feat: new has_in_common expression --- core/array_test.go | 167 ++++++++++++++++++++++++++++++ core/insert_test.go | 45 -------- core/internal/psql/exp.go | 53 +++++----- core/internal/qcode/exp.go | 7 +- core/internal/qcode/gen_string.go | 35 ++++--- core/internal/qcode/qcode.go | 1 + core/query1_test.go | 42 -------- core/query2_test.go | 35 ------- 8 files changed, 218 insertions(+), 167 deletions(-) create mode 100644 core/array_test.go diff --git a/core/array_test.go b/core/array_test.go new file mode 100644 index 00000000..e621380c --- /dev/null +++ b/core/array_test.go @@ -0,0 +1,167 @@ +//go:build !mysql + +package core_test + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/dosco/graphjin/core" +) + +func Example_queryParentAndChildrenViaArrayColumn() { + gql := ` + query { + products(limit: 2) { + name + price + categories { + id + name + } + } + categories { + name + products { + name + } + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true, DefaultLimit: 2}) + conf.Tables = []core.Table{{ + Name: "products", + Columns: []core.Column{ + {Name: "category_ids", ForeignKey: "categories.id", Array: true}, + }, + }, + } + + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + res, err := gj.GraphQL(context.Background(), gql, nil, nil) + if err != nil { + fmt.Println(err) + } else { + printJSON(res.Data) + } + // Output: {"categories":[{"name":"Category 1","products":[{"name":"Product 1"},{"name":"Product 2"}]},{"name":"Category 2","products":[{"name":"Product 1"},{"name":"Product 2"}]}],"products":[{"categories":[{"id":1,"name":"Category 1"},{"id":2,"name":"Category 2"}],"name":"Product 1","price":11.5},{"categories":[{"id":1,"name":"Category 1"},{"id":2,"name":"Category 2"}],"name":"Product 2","price":12.5}]} +} + +func Example_insertIntoTableAndConnectToRelatedTableWithArrayColumn() { + gql := `mutation { + products(insert: $data) { + id + name + categories { + id + name + } + } + }` + + vars := json.RawMessage(`{ + "data": { + "id": 2006, + "name": "Product 2006", + "description": "Description for product 2006", + "price": 2016.5, + "tags": ["Tag 1", "Tag 2"], + "categories": { + "connect": { "id": [1, 2, 3, 4, 5] } + } + } + }`) + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + conf.Tables = []core.Table{ + {Name: "products", Columns: []core.Column{{Name: "category_ids", ForeignKey: "categories.id"}}}, + } + + gj, err := core.NewGraphJin(conf, db) + if err != nil { + panic(err) + } + + ctx := context.WithValue(context.Background(), core.UserIDKey, 3) + res, err := gj.GraphQL(ctx, gql, vars, nil) + if err != nil { + fmt.Println(err) + } else { + printJSON(res.Data) + } + // Output: {"products":[{"categories":[{"id":1,"name":"Category 1"},{"id":2,"name":"Category 2"},{"id":3,"name":"Category 3"},{"id":4,"name":"Category 4"},{"id":5,"name":"Category 5"}],"id":2006,"name":"Product 2006"}]} +} + +// TODO: Fix: Does not work in MYSQL +func Example_veryComplexQueryWithArrayColumns() { + gql := `query { + products( + # returns only 1 items + limit: 1, + + # starts from item 10, commented out for now + # offset: 10, + + # orders the response items by highest price + order_by: { price: desc }, + + # only items with an id >= 30 and < 30 are returned + where: { id: { and: { greater_or_equals: 20, lt: 28 } } }) { + id + name + price + owner { + full_name + picture : avatar + email + category_counts(limit: 2) { + count + category { + name + } + } + } + category(limit: 2) { + id + name + } + } + }` + + conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) + conf.Tables = []core.Table{ + { + Name: "category_counts", + Table: "users", + Type: "json", + Columns: []core.Column{ + {Name: "category_id", Type: "int", ForeignKey: "categories.id"}, + {Name: "count", Type: "int"}, + }, + }, + { + Name: "products", + Columns: []core.Column{{Name: "category_ids", ForeignKey: "categories.id"}}, + }, + } + + gj, err := core.NewGraphJin(conf, db) + if err != nil { + fmt.Println(err) + return + } + + res, err := gj.GraphQL(context.Background(), gql, nil, nil) + if err != nil { + fmt.Println(err) + return + } + + printJSON(res.Data) + // Output: {"products":[{"category":[{"id":1,"name":"Category 1"},{"id":2,"name":"Category 2"}],"id":27,"name":"Product 27","owner":{"category_counts":[{"category":{"name":"Category 1"},"count":400},{"category":{"name":"Category 2"},"count":600}],"email":"user27@test.com","full_name":"User 27","picture":null},"price":37.5}]} +} diff --git a/core/insert_test.go b/core/insert_test.go index e853b1b7..23013e79 100644 --- a/core/insert_test.go +++ b/core/insert_test.go @@ -490,51 +490,6 @@ func Example_insertIntoTableAndConnectToRelatedTables() { // Output: {"products":[{"id":2005,"name":"Product 2005","owner":{"email":"user6@test.com","full_name":"User 6","id":6}}]} } -func Example_insertIntoTableAndConnectToRelatedTableWithArrayColumn() { - gql := `mutation { - products(insert: $data) { - id - name - categories { - id - name - } - } - }` - - vars := json.RawMessage(`{ - "data": { - "id": 2006, - "name": "Product 2006", - "description": "Description for product 2006", - "price": 2016.5, - "tags": ["Tag 1", "Tag 2"], - "categories": { - "connect": { "id": [1, 2, 3, 4, 5] } - } - } - }`) - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - conf.Tables = []core.Table{ - {Name: "products", Columns: []core.Column{{Name: "category_ids", ForeignKey: "categories.id"}}}, - } - - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - ctx := context.WithValue(context.Background(), core.UserIDKey, 3) - res, err := gj.GraphQL(ctx, gql, vars, nil) - if err != nil { - fmt.Println(err) - } else { - printJSON(res.Data) - } - // Output: {"products":[{"categories":[{"id":1,"name":"Category 1"},{"id":2,"name":"Category 2"},{"id":3,"name":"Category 3"},{"id":4,"name":"Category 4"},{"id":5,"name":"Category 5"}],"id":2006,"name":"Product 2006"}]} -} - func Example_insertWithCamelToSnakeCase() { gql := `mutation { products(insert: $data) { diff --git a/core/internal/psql/exp.go b/core/internal/psql/exp.go index 9bc0532e..5c8df088 100644 --- a/core/internal/psql/exp.go +++ b/core/internal/psql/exp.go @@ -206,6 +206,8 @@ func (c *expContext) renderOp(ex *qcode.Exp) { c.w.WriteString(`@>`) case qcode.OpContainedIn: c.w.WriteString(`<@`) + case qcode.OpHasInCommon: + c.w.WriteString(`&&`) case qcode.OpHasKey: c.w.WriteString(`?`) case qcode.OpHasKeyAny: @@ -288,9 +290,10 @@ func (c *expContext) renderOp(ex *qcode.Exp) { } func (c *expContext) renderValPrefix(ex *qcode.Exp) bool { - if c.ct == "mysql" && (ex.Op == qcode.OpHasKey || + switch { + case c.ct == "mysql" && (ex.Op == qcode.OpHasKey || ex.Op == qcode.OpHasKeyAny || - ex.Op == qcode.OpHasKeyAll) { + ex.Op == qcode.OpHasKeyAll): var optype string switch ex.Op { case qcode.OpHasKey, qcode.OpHasKeyAny: @@ -306,30 +309,34 @@ func (c *expContext) renderValPrefix(ex *qcode.Exp) bool { } c.w.WriteString(") = 1") return true - } - - if ex.Right.ValType == qcode.ValVar { - return c.renderValVarPrefix(ex) - } - return false -} -func (c *expContext) renderValVarPrefix(ex *qcode.Exp) bool { - if ex.Op == qcode.OpIn || ex.Op == qcode.OpNotIn { - if c.ct == "mysql" { - c.w.WriteString(`JSON_CONTAINS(`) - c.renderParam(Param{Name: ex.Right.Val, Type: ex.Left.Col.Type, IsArray: true}) - c.w.WriteString(`, CAST(`) - c.colWithTable(c.ti.Name, ex.Left.Col.Name) - c.w.WriteString(` AS JSON), '$')`) - return true - } + case c.ct == "mysql" && ex.Right.ValType == qcode.ValVar && + (ex.Op == qcode.OpIn || ex.Op == qcode.OpNotIn): + c.w.WriteString(`JSON_CONTAINS(`) + c.renderParam(Param{Name: ex.Right.Val, Type: ex.Left.Col.Type, IsArray: true}) + c.w.WriteString(`, CAST(`) + c.colWithTable(c.ti.Name, ex.Left.Col.Name) + c.w.WriteString(` AS JSON), '$')`) + return true } return false } func (c *expContext) renderVal(ex *qcode.Exp) { - if ex.Right.Col.Name != "" { + switch { + case ex.Right.ValType == qcode.ValVar: + c.renderValVar(ex) + + case !ex.Right.Col.Array && (ex.Op == qcode.OpContains || + ex.Op == qcode.OpContainedIn || + ex.Op == qcode.OpHasInCommon): + c.w.WriteString(`CAST(ARRAY[`) + c.colWithTable(c.ti.Name, ex.Right.Col.Name) + c.w.WriteString(`] AS `) + c.w.WriteString(ex.Right.Col.Type) + c.w.WriteString(`[])`) + + case ex.Right.Col.Name != "": var table string if ex.Right.Table == "" { table = ex.Right.Col.Table @@ -353,12 +360,6 @@ func (c *expContext) renderVal(ex *qcode.Exp) { } } c.w.WriteString(`)`) - return - } - - switch ex.Right.ValType { - case qcode.ValVar: - c.renderValVar(ex) default: if len(ex.Right.Path) == 0 { diff --git a/core/internal/qcode/exp.go b/core/internal/qcode/exp.go index 56e7d193..cc81030a 100644 --- a/core/internal/qcode/exp.go +++ b/core/internal/qcode/exp.go @@ -198,7 +198,7 @@ func (ast *aexpst) parseNode(av aexp, node *graph.Node) (*Exp, error) { } setListVal(ex, node) if ex.Left.Col.Array { - ex.Op = OpContains + ex.Op = OpHasInCommon } else { ex.Op = OpIn } @@ -209,7 +209,7 @@ func (ast *aexpst) parseNode(av aexp, node *graph.Node) (*Exp, error) { return nil, err } if ex.Left.Col.Array { - ex.Op = OpContains + ex.Op = OpHasInCommon setListVal(ex, node) } else { if ex.Right.ValType, err = getExpType(node); err != nil { @@ -354,6 +354,9 @@ func (ast *aexpst) processOpAndVal(av aexp, ex *Exp, node *graph.Node) (bool, er case "contained_in": ex.Op = OpContainedIn setListVal(ex, node) + case "has_in_common": + ex.Op = OpHasInCommon + setListVal(ex, node) case "has_key": ex.Op = OpHasKey ex.Right.Val = node.Val diff --git a/core/internal/qcode/gen_string.go b/core/internal/qcode/gen_string.go index a160301b..c2eb48c2 100644 --- a/core/internal/qcode/gen_string.go +++ b/core/internal/qcode/gen_string.go @@ -184,23 +184,24 @@ func _() { _ = x[OpNotIRegex-21] _ = x[OpContains-22] _ = x[OpContainedIn-23] - _ = x[OpHasKey-24] - _ = x[OpHasKeyAny-25] - _ = x[OpHasKeyAll-26] - _ = x[OpIsNull-27] - _ = x[OpIsNotNull-28] - _ = x[OpTsQuery-29] - _ = x[OpFalse-30] - _ = x[OpNotDistinct-31] - _ = x[OpDistinct-32] - _ = x[OpEqualsTrue-33] - _ = x[OpNotEqualsTrue-34] - _ = x[OpSelectExists-35] -} - -const _ExpOp_name = "OpNopOpAndOpOrOpNotOpEqualsOpNotEqualsOpGreaterOrEqualsOpLesserOrEqualsOpGreaterThanOpLesserThanOpInOpNotInOpLikeOpNotLikeOpILikeOpNotILikeOpSimilarOpNotSimilarOpRegexOpNotRegexOpIRegexOpNotIRegexOpContainsOpContainedInOpHasKeyOpHasKeyAnyOpHasKeyAllOpIsNullOpIsNotNullOpTsQueryOpFalseOpNotDistinctOpDistinctOpEqualsTrueOpNotEqualsTrueOpSelectExists" - -var _ExpOp_index = [...]uint16{0, 5, 10, 14, 19, 27, 38, 55, 71, 84, 96, 100, 107, 113, 122, 129, 139, 148, 160, 167, 177, 185, 196, 206, 219, 227, 238, 249, 257, 268, 277, 284, 297, 307, 319, 334, 348} + _ = x[OpHasInCommon-24] + _ = x[OpHasKey-25] + _ = x[OpHasKeyAny-26] + _ = x[OpHasKeyAll-27] + _ = x[OpIsNull-28] + _ = x[OpIsNotNull-29] + _ = x[OpTsQuery-30] + _ = x[OpFalse-31] + _ = x[OpNotDistinct-32] + _ = x[OpDistinct-33] + _ = x[OpEqualsTrue-34] + _ = x[OpNotEqualsTrue-35] + _ = x[OpSelectExists-36] +} + +const _ExpOp_name = "OpNopOpAndOpOrOpNotOpEqualsOpNotEqualsOpGreaterOrEqualsOpLesserOrEqualsOpGreaterThanOpLesserThanOpInOpNotInOpLikeOpNotLikeOpILikeOpNotILikeOpSimilarOpNotSimilarOpRegexOpNotRegexOpIRegexOpNotIRegexOpContainsOpContainedInOpHasInCommonOpHasKeyOpHasKeyAnyOpHasKeyAllOpIsNullOpIsNotNullOpTsQueryOpFalseOpNotDistinctOpDistinctOpEqualsTrueOpNotEqualsTrueOpSelectExists" + +var _ExpOp_index = [...]uint16{0, 5, 10, 14, 19, 27, 38, 55, 71, 84, 96, 100, 107, 113, 122, 129, 139, 148, 160, 167, 177, 185, 196, 206, 219, 232, 240, 251, 262, 270, 281, 290, 297, 310, 320, 332, 347, 361} func (i ExpOp) String() string { if i < 0 || i >= ExpOp(len(_ExpOp_index)-1) { diff --git a/core/internal/qcode/qcode.go b/core/internal/qcode/qcode.go index 99a41140..c01cbef7 100644 --- a/core/internal/qcode/qcode.go +++ b/core/internal/qcode/qcode.go @@ -222,6 +222,7 @@ const ( OpNotIRegex OpContains OpContainedIn + OpHasInCommon OpHasKey OpHasKeyAny OpHasKeyAll diff --git a/core/query1_test.go b/core/query1_test.go index 23e1be60..d19d961f 100644 --- a/core/query1_test.go +++ b/core/query1_test.go @@ -457,48 +457,6 @@ func Example_queryChildrenWithParent() { // Output: {"products":[{"name":"Product 1","owner":{"email":"user1@test.com"},"price":11.5},{"name":"Product 2","owner":{"email":"user2@test.com"},"price":12.5}]} } -func Example_queryParentAndChildrenViaArrayColumn() { - gql := ` - query { - products(limit: 2) { - name - price - categories { - id - name - } - } - categories { - name - products { - name - } - } - }` - - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true, DefaultLimit: 2}) - conf.Tables = []core.Table{{ - Name: "products", - Columns: []core.Column{ - {Name: "category_ids", ForeignKey: "categories.id", Array: true}, - }, - }, - } - - gj, err := core.NewGraphJin(conf, db) - if err != nil { - panic(err) - } - - res, err := gj.GraphQL(context.Background(), gql, nil, nil) - if err != nil { - fmt.Println(err) - } else { - printJSON(res.Data) - } - // Output: {"categories":[{"name":"Category 1","products":[{"name":"Product 1"},{"name":"Product 2"}]},{"name":"Category 2","products":[{"name":"Product 1"},{"name":"Product 2"}]}],"products":[{"categories":[{"id":1,"name":"Category 1"},{"id":2,"name":"Category 2"}],"name":"Product 1","price":11.5},{"categories":[{"id":1,"name":"Category 1"},{"id":2,"name":"Category 2"}],"name":"Product 2","price":12.5}]} -} - func Example_queryManyToManyViaJoinTable1() { gql := `query { products(limit: 2) { diff --git a/core/query2_test.go b/core/query2_test.go index 6cb96a44..db3a4d97 100644 --- a/core/query2_test.go +++ b/core/query2_test.go @@ -331,41 +331,6 @@ var benchGQL = `query { } }` -// TODO: Fix: Does not work in MYSQL -func Example_veryComplexQuery() { - conf := newConfig(&core.Config{DBType: dbType, DisableAllowList: true}) - conf.Tables = []core.Table{ - { - Name: "category_counts", - Table: "users", - Type: "json", - Columns: []core.Column{ - {Name: "category_id", Type: "int", ForeignKey: "categories.id"}, - {Name: "count", Type: "int"}, - }, - }, - { - Name: "products", - Columns: []core.Column{{Name: "category_ids", ForeignKey: "categories.id"}}, - }, - } - - gj, err := core.NewGraphJin(conf, db) - if err != nil { - fmt.Println(err) - return - } - - res, err := gj.GraphQL(context.Background(), benchGQL, nil, nil) - if err != nil { - fmt.Println(err) - return - } - - printJSON(res.Data) - // Output: {"products":[{"category":[{"id":1,"name":"Category 1"},{"id":2,"name":"Category 2"}],"id":27,"name":"Product 27","owner":{"category_counts":[{"category":{"name":"Category 1"},"count":400},{"category":{"name":"Category 2"},"count":600}],"email":"user27@test.com","full_name":"User 27","picture":null},"price":37.5}]} -} - var resultJSON json.RawMessage func BenchmarkCompile(b *testing.B) {