diff --git a/docstore/driver/driver.go b/docstore/driver/driver.go index bd2bbccb0e..716012d650 100644 --- a/docstore/driver/driver.go +++ b/docstore/driver/driver.go @@ -219,7 +219,7 @@ type Query struct { // TODO(#1762): support comparison of other types. type Filter struct { FieldPath []string // the field path to filter - Op string // the operation, supports `=`, `>`, `>=`, `<`, `<=`, `in`, `not-in` + Op string // the operation, supports `=`, `>`, `>=`, `<`, `<=`, `in`, `not-in`, `array-contains` Value interface{} // the value to compare using the operation } diff --git a/docstore/drivertest/drivertest.go b/docstore/drivertest/drivertest.go index 404d33f056..83ed96e08b 100644 --- a/docstore/drivertest/drivertest.go +++ b/docstore/drivertest/drivertest.go @@ -1273,6 +1273,7 @@ type HighScore struct { Score int Time time.Time WithGlitch bool + GameCategories []string DocstoreRevision interface{} } @@ -1316,15 +1317,21 @@ const ( game3 = "Days Gone" ) +var ( + game1Category = []string{"Action", "Adventure"} + game2Category = []string{"Horror", "Comedy"} + game3Category = []string{"Action", "Adventure", "Horror"} +) + var highScores = []*HighScore{ - {game1, "pat", 49, date(3, 13), false, nil}, - {game1, "mel", 60, date(4, 10), false, nil}, - {game1, "andy", 81, date(2, 1), false, nil}, - {game1, "fran", 33, date(3, 19), false, nil}, - {game2, "pat", 120, date(4, 1), true, nil}, - {game2, "billie", 111, date(4, 10), false, nil}, - {game2, "mel", 190, date(4, 18), true, nil}, - {game2, "fran", 33, date(3, 20), false, nil}, + {game1, "pat", 49, date(3, 13), game1Category, nil}, + {game1, "mel", 60, date(4, 10), game1Category, nil}, + {game1, "andy", 81, date(2, 1), game1Category, nil}, + {game1, "fran", 33, date(3, 19), game1Category, nil}, + {game2, "pat", 120, date(4, 1), game2Category, nil}, + {game2, "billie", 111, date(4, 10), game2Category, nil}, + {game2, "mel", 190, date(4, 18), game2Category, nil}, + {game2, "fran", 33, date(3, 20), game2Category, nil}, } func addHighScores(t *testing.T, coll *docstore.Collection) { @@ -1491,6 +1498,11 @@ func testGetQuery(t *testing.T, _ Harness, coll *docstore.Collection) { q: coll.Query().Where("WithGlitch", "=", true), want: func(h *HighScore) bool { return h.WithGlitch }, }, + { + name: "GameCategories", + q: coll.Query().Where("GameCategories", "array-contains", game1Category[0]), + want: func(h *HighScore) bool { return arrayContains(h.GameCategories, game1Category[0]) }, + }, { name: "AllByPlayerAsc", q: coll.Query().OrderBy("Player", docstore.Ascending), @@ -1529,13 +1541,14 @@ func testGetQuery(t *testing.T, _ Harness, coll *docstore.Collection) { h.Score = 0 h.Time = time.Time{} h.WithGlitch = false + h.GameCategories = nil return true }, }, { name: "AllWithScore", q: coll.Query(), - fields: []docstore.FieldPath{"Game", "Player", "Score", "WithGlitch", docstore.FieldPath(docstore.DefaultRevisionField)}, + fields: []docstore.FieldPath{"Game", "Player", "Score", "WithGlitch", "GameCategories", docstore.FieldPath(docstore.DefaultRevisionField)}, want: func(h *HighScore) bool { h.Time = time.Time{} return true @@ -2125,7 +2138,7 @@ func testAs(t *testing.T, coll *docstore.Collection, st AsTest) { } // ErrorCheck - doc := &HighScore{game3, "steph", 24, date(4, 25), false, nil} + doc := &HighScore{game3, "steph", 24, date(4, 25), game3Category, nil} if err := coll.Create(ctx, doc); err != nil { t.Fatal(err) } @@ -2164,3 +2177,12 @@ func checkCode(t *testing.T, err error, code gcerrors.ErrorCode) { t.Errorf("got %v, want %s", err, code) } } + +func arrayContains[T comparable](a []T, x T) bool { + for _, e := range a { + if e == x { + return true + } + } + return false +} diff --git a/docstore/gcpfirestore/query.go b/docstore/gcpfirestore/query.go index ea714c134e..bd63525da5 100644 --- a/docstore/gcpfirestore/query.go +++ b/docstore/gcpfirestore/query.go @@ -274,7 +274,7 @@ func splitFilters(fs []driver.Filter) (sendToFirestore, evaluateLocally []driver // Enforce that only one field can have an inequality. var rangeFP []string for _, f := range fs { - if f.Op == driver.EqualOp { + if f.Op == driver.EqualOp || f.Op == "array-contains" { sendToFirestore = append(sendToFirestore, f) } else { if rangeFP == nil || driver.FieldPathsEqual(rangeFP, f.FieldPath) { @@ -366,9 +366,8 @@ func newFieldFilter(fp []string, op string, val *pb.Value) (*pb.StructuredQuery_ fop = pb.StructuredQuery_FieldFilter_IN case "not-in": fop = pb.StructuredQuery_FieldFilter_NOT_IN - // TODO(jba): can we support array-contains portably? - // case "array-contains": - // fop = pb.StructuredQuery_FieldFilter_ARRAY_CONTAINS + case "array-contains": + fop = pb.StructuredQuery_FieldFilter_ARRAY_CONTAINS default: return nil, gcerr.Newf(gcerr.InvalidArgument, nil, "invalid operator: %q", op) } diff --git a/docstore/gcpfirestore/testdata/TestConformance/ActionsOnStructNoRev.replay b/docstore/gcpfirestore/testdata/TestConformance/ActionsOnStructNoRev.replay index 2ac1e8087b..96cbebb733 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/ActionsOnStructNoRev.replay and b/docstore/gcpfirestore/testdata/TestConformance/ActionsOnStructNoRev.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/ActionsWithCompositeID.replay b/docstore/gcpfirestore/testdata/TestConformance/ActionsWithCompositeID.replay index aeed678980..1004a0e833 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/ActionsWithCompositeID.replay and b/docstore/gcpfirestore/testdata/TestConformance/ActionsWithCompositeID.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/As/verify_As.replay b/docstore/gcpfirestore/testdata/TestConformance/As/verify_As.replay index 3bf62c7c14..a47e7daa84 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/As/verify_As.replay and b/docstore/gcpfirestore/testdata/TestConformance/As/verify_As.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/As/verify_As_returns_false_when_passed_nil.replay b/docstore/gcpfirestore/testdata/TestConformance/As/verify_As_returns_false_when_passed_nil.replay index ab3bb81c5c..6a6fad65d6 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/As/verify_As_returns_false_when_passed_nil.replay and b/docstore/gcpfirestore/testdata/TestConformance/As/verify_As_returns_false_when_passed_nil.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/BeforeDo.replay b/docstore/gcpfirestore/testdata/TestConformance/BeforeDo.replay index 82c05c124d..2a61e3991b 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/BeforeDo.replay and b/docstore/gcpfirestore/testdata/TestConformance/BeforeDo.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/BeforeQuery.replay b/docstore/gcpfirestore/testdata/TestConformance/BeforeQuery.replay index ac23b70c47..c1f85c5002 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/BeforeQuery.replay and b/docstore/gcpfirestore/testdata/TestConformance/BeforeQuery.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/Create.replay b/docstore/gcpfirestore/testdata/TestConformance/Create.replay index 582d61b269..495507bd4b 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/Create.replay and b/docstore/gcpfirestore/testdata/TestConformance/Create.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/Data.replay b/docstore/gcpfirestore/testdata/TestConformance/Data.replay index d52c06e896..8d371361ab 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/Data.replay and b/docstore/gcpfirestore/testdata/TestConformance/Data.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/Delete.replay b/docstore/gcpfirestore/testdata/TestConformance/Delete.replay index 4b063a5529..b07e36f13d 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/Delete.replay and b/docstore/gcpfirestore/testdata/TestConformance/Delete.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/ExampleInDoc.replay b/docstore/gcpfirestore/testdata/TestConformance/ExampleInDoc.replay index 9249ac00d3..c041cb10be 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/ExampleInDoc.replay and b/docstore/gcpfirestore/testdata/TestConformance/ExampleInDoc.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/Get.replay b/docstore/gcpfirestore/testdata/TestConformance/Get.replay index 4d496e162b..cf585f3b7e 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/Get.replay and b/docstore/gcpfirestore/testdata/TestConformance/Get.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/GetQuery.replay b/docstore/gcpfirestore/testdata/TestConformance/GetQuery.replay index 09f89a1c38..3adade2d4e 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/GetQuery.replay and b/docstore/gcpfirestore/testdata/TestConformance/GetQuery.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/GetQueryKeyField.replay b/docstore/gcpfirestore/testdata/TestConformance/GetQueryKeyField.replay index c5f665d07e..abab6d1810 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/GetQueryKeyField.replay and b/docstore/gcpfirestore/testdata/TestConformance/GetQueryKeyField.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/MultipleActions.replay b/docstore/gcpfirestore/testdata/TestConformance/MultipleActions.replay index d34dda47c8..24bafae5d8 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/MultipleActions.replay and b/docstore/gcpfirestore/testdata/TestConformance/MultipleActions.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/Proto.replay b/docstore/gcpfirestore/testdata/TestConformance/Proto.replay index 1751d8667b..e50a41fbe2 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/Proto.replay and b/docstore/gcpfirestore/testdata/TestConformance/Proto.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/Put.replay b/docstore/gcpfirestore/testdata/TestConformance/Put.replay index 6f7e41430e..0517311ecb 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/Put.replay and b/docstore/gcpfirestore/testdata/TestConformance/Put.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/Replace.replay b/docstore/gcpfirestore/testdata/TestConformance/Replace.replay index 3b52fc319c..a074a39e87 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/Replace.replay and b/docstore/gcpfirestore/testdata/TestConformance/Replace.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/SerializeRevision.replay b/docstore/gcpfirestore/testdata/TestConformance/SerializeRevision.replay index 8714ca8aec..8f0bbb6cfa 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/SerializeRevision.replay and b/docstore/gcpfirestore/testdata/TestConformance/SerializeRevision.replay differ diff --git a/docstore/gcpfirestore/testdata/TestConformance/Update.replay b/docstore/gcpfirestore/testdata/TestConformance/Update.replay index 79379e9ee8..f1c564f58f 100644 Binary files a/docstore/gcpfirestore/testdata/TestConformance/Update.replay and b/docstore/gcpfirestore/testdata/TestConformance/Update.replay differ diff --git a/docstore/memdocstore/query.go b/docstore/memdocstore/query.go index 419017b993..72495d5987 100644 --- a/docstore/memdocstore/query.go +++ b/docstore/memdocstore/query.go @@ -111,6 +111,8 @@ func applyComparison(op string, c int) bool { return c >= 0 case "<=": return c <= 0 + case "array-contains": + return c == 0 case "in": return c == 0 case "not-in": @@ -123,6 +125,11 @@ func applyComparison(op string, c int) bool { func compare(x1, x2 interface{}) (int, bool) { v1 := reflect.ValueOf(x1) v2 := reflect.ValueOf(x2) + // this if for array-contains queries. We are just inverting the parameters x1 and x2 as it's the same logic + // as the in/not-in queries. + if v1.Kind() == reflect.Slice { + return compare(x2, x1) + } // this is for in/not-in queries. // return 0 if x1 is in slice x2, -1 if not. if v2.Kind() == reflect.Slice { diff --git a/docstore/mongodocstore/query.go b/docstore/mongodocstore/query.go index 0405136990..62cdf6f4ed 100644 --- a/docstore/mongodocstore/query.go +++ b/docstore/mongodocstore/query.go @@ -74,13 +74,14 @@ func (c *collection) RunGetQuery(ctx context.Context, q *driver.Query) (driver.D } var mongoQueryOps = map[string]string{ - driver.EqualOp: "$eq", - ">": "$gt", - ">=": "$gte", - "<": "$lt", - "<=": "$lte", - "in": "$in", - "not-in": "$nin", + driver.EqualOp: "$eq", + ">": "$gt", + ">=": "$gte", + "<": "$lt", + "<=": "$lte", + "in": "$in", + "not-in": "$nin", + "array-contains": "$eq", } // filtersToBSON converts a []driver.Filter to the MongoDB equivalent, expressed diff --git a/docstore/query.go b/docstore/query.go index 847d3ae8cc..17f85ece38 100644 --- a/docstore/query.go +++ b/docstore/query.go @@ -37,8 +37,8 @@ func (c *Collection) Query() *Query { } // Where expresses a condition on the query. -// Valid ops are: "=", ">", "<", ">=", "<=, "in", "not-in". -// Valid values are strings, integers, floating-point numbers, time.Time and boolean (only for "=", "in" and "not-in") values. +// Valid ops are: "=", ">", "<", ">=", "<=, "in", "not-in", "array-contains". +// Valid values are strings, integers, floating-point numbers, and time.Time and boolean (only for "=", "in" and "not-in") values. func (q *Query) Where(fp FieldPath, op string, value interface{}) *Query { if q.err != nil { return q @@ -50,7 +50,7 @@ func (q *Query) Where(fp FieldPath, op string, value interface{}) *Query { } validator, ok := validOp[op] if !ok { - return q.invalidf("invalid filter operator: %q. Use one of: =, >, <, >=, <=, in, not-in", op) + return q.invalidf("invalid filter operator: %q. Use one of: =, >, <, >=, <=, in, not-in, array-contains", op) } if !validator(value) { return q.invalidf("invalid filter value: %v", value) @@ -66,13 +66,14 @@ func (q *Query) Where(fp FieldPath, op string, value interface{}) *Query { type valueValidator func(interface{}) bool var validOp = map[string]valueValidator{ - "=": validEqualValue, - ">": validFilterValue, - "<": validFilterValue, - ">=": validFilterValue, - "<=": validFilterValue, - "in": validFilterSlice, - "not-in": validFilterSlice, + "=": validEqualValue, + ">": validFilterValue, + "<": validFilterValue, + ">=": validFilterValue, + "<=": validFilterValue, + "array-contains": validFilterValue, + "in": validFilterSlice, + "not-in": validFilterSlice, } func validEqualValue(v interface{}) bool {