From ae377932de20d99f31766ca1cccd2d1cfa18a1c0 Mon Sep 17 00:00:00 2001 From: Baha Aiman Date: Fri, 21 Apr 2023 22:35:45 +0530 Subject: [PATCH] feat(firestore): EntityFilter for AND/OR queries (#7757) * feat(datastore): EntityFilter for AND/OR queries --- firestore/integration_test.go | 218 +++++++++++++++++++++++++++++- firestore/query.go | 172 +++++++++++++++++++++--- firestore/query_test.go | 242 ++++++++++++++++++++++++++++++++-- firestore/util_test.go | 2 +- 4 files changed, 603 insertions(+), 31 deletions(-) diff --git a/firestore/integration_test.go b/firestore/integration_test.go index 052fca0e7751..8b62abf8266e 100644 --- a/firestore/integration_test.go +++ b/firestore/integration_test.go @@ -26,9 +26,12 @@ import ( "reflect" "runtime" "sort" + "sync" "testing" "time" + apiv1 "cloud.google.com/go/firestore/apiv1/admin" + "cloud.google.com/go/firestore/apiv1/admin/adminpb" firestorev1 "cloud.google.com/go/firestore/apiv1/firestorepb" "cloud.google.com/go/internal/pretty" "cloud.google.com/go/internal/testutil" @@ -55,6 +58,7 @@ const ( var ( iClient *Client + iAdminClient *apiv1.FirestoreAdminClient iColl *CollectionRef collectionIDs = uid.NewSpace("go-integration-test", nil) ) @@ -76,7 +80,8 @@ func initIntegrationTest() { if ts == nil { log.Fatal("The project key must be set. See CONTRIBUTING.md for details") } - wantDBPath := "projects/" + testProjectID + "/databases/(default)" + projectPath := "projects/" + testProjectID + wantDBPath := projectPath + "/databases/(default)" ti := &testutil.HeadersEnforcer{ Checkers: []*testutil.HeaderChecker{ @@ -103,18 +108,74 @@ func initIntegrationTest() { } iClient = c iColl = c.Collection(collectionIDs.New()) + + adminC, err := apiv1.NewFirestoreAdminClient(ctx, option.WithTokenSource(ts)) + if err != nil { + log.Fatalf("NewFirestoreAdminClient: %v", err) + } + iAdminClient = adminC + + createIndexes(ctx, wantDBPath) + refDoc := iColl.NewDoc() integrationTestMap["ref"] = refDoc wantIntegrationTestMap["ref"] = refDoc integrationTestStruct.Ref = refDoc } +// createIndexes creates composite indexes on provided Firestore database +// Indexes are required to run queries with composite filters on multiple fields. +// Without indexes, FailedPrecondition rpc error is seen with +// desc 'The query requires multiple indexes'. +func createIndexes(ctx context.Context, dbPath string) { + var createIndexWg sync.WaitGroup + + indexFields := [][]string{{"updatedAt", "weight", "height"}, {"weight", "height"}} + indexParent := fmt.Sprintf("%s/collectionGroups/%s", dbPath, iColl.ID) + + createIndexWg.Add(len(indexFields)) + for _, fields := range indexFields { + var adminPbIndexFields []*adminpb.Index_IndexField + for _, field := range fields { + adminPbIndexFields = append(adminPbIndexFields, &adminpb.Index_IndexField{ + FieldPath: field, + ValueMode: &adminpb.Index_IndexField_Order_{ + Order: adminpb.Index_IndexField_ASCENDING, + }, + }) + } + req := &adminpb.CreateIndexRequest{ + Parent: indexParent, + Index: &adminpb.Index{ + QueryScope: adminpb.Index_COLLECTION, + Fields: adminPbIndexFields, + }, + } + go func(req *adminpb.CreateIndexRequest) { + op, err := iAdminClient.CreateIndex(ctx, req) + if err != nil { + log.Fatalf("CreateIndex: %v", err) + } + + _, err = op.Wait(ctx) + if err != nil { + log.Fatalf("Wait: %v", err) + } + createIndexWg.Done() + }(req) + } + createIndexWg.Wait() +} + func cleanupIntegrationTest() { - if iClient == nil { - return + if iClient != nil { + // TODO(jba): delete everything in integrationColl. + iClient.Close() + } + + if iAdminClient != nil { + iAdminClient.Close() } - // TODO(jba): delete everything in integrationColl. - iClient.Close() } // integrationClient should be called by integration tests to get a valid client. It will never @@ -646,6 +707,153 @@ func TestIntegration_WriteBatch(t *testing.T) { // TODO(jba): test verify when it is supported. } +func TestIntegration_QueryDocuments_WhereEntity(t *testing.T) { + ctx := context.Background() + coll := integrationColl(t) + h := testHelper{t} + nowTime := time.Now() + todayTime := nowTime.Unix() + yesterdayTime := nowTime.AddDate(0, 0, -1).Unix() + docs := []map[string]interface{}{ + // To support running this test in parallel with the others, use a field name + // that we don't use anywhere else. + {"height": 1, "weight": 99, "updatedAt": yesterdayTime}, + {"height": 2, "weight": 98, "updatedAt": yesterdayTime}, + {"height": 3, "weight": 97, "updatedAt": yesterdayTime}, + {"height": 4, "weight": 96, "updatedAt": todayTime}, + {"height": 5, "weight": 95, "updatedAt": todayTime}, + {"height": 6, "weight": 94, "updatedAt": todayTime}, + {"height": 7, "weight": 93, "updatedAt": todayTime}, + {"height": 8, "weight": 93, "updatedAt": todayTime}, + } + var wants []map[string]interface{} + for _, doc := range docs { + newDoc := coll.NewDoc() + wants = append(wants, map[string]interface{}{ + "height": int64(doc["height"].(int)), + "weight": int64(doc["weight"].(int)), + "updatedAt": doc["updatedAt"].(int64), + }) + h.mustCreate(newDoc, doc) + } + + q := coll.Select("height", "weight", "updatedAt") + for i, test := range []struct { + desc string + q Query + want []map[string]interface{} + orderBy bool // Some query types do not allow ordering. + }{ + { + desc: "height == 5", + q: q.WhereEntity(PropertyFilter{ + Path: "height", + Operator: "==", + Value: 5, + }), + want: wants[4:5], + orderBy: false, + }, + { + desc: "height > 1", + q: q.WhereEntity(PropertyFilter{ + Path: "height", + Operator: ">", + Value: 1, + }), + want: wants[1:], + orderBy: true, + }, + + {desc: "((weight > 97 AND updatedAt == yesterdayTime) OR (weight < 94)) AND height == 8", + q: q.WhereEntity( + AndFilter{ + Filters: []EntityFilter{ + OrFilter{ + Filters: []EntityFilter{ + AndFilter{ + []EntityFilter{ + PropertyFilter{Path: "height", Operator: "<", Value: 3}, + PropertyFilter{Path: "updatedAt", Operator: "==", Value: yesterdayTime}, + }, + }, + PropertyFilter{Path: "height", Operator: ">", Value: 6}, + }, + }, + PropertyFilter{Path: "weight", Operator: "==", Value: 93}, + }, + }, + ), + want: wants[6:], + orderBy: true, + }, + { + desc: "height > 5 OR height < 8", + q: q.WhereEntity( + AndFilter{ + Filters: []EntityFilter{ + PropertyFilter{ + Path: "height", + Operator: ">", + Value: 5, + }, + PropertyFilter{ + Path: "height", + Operator: "<", + Value: 8, + }, + }, + }, + ), + want: wants[5:7], + orderBy: true, + }, + { + desc: "height <= 2 OR height > 7", + q: q.WhereEntity( + OrFilter{ + Filters: []EntityFilter{ + PropertyFilter{ + Path: "height", + Operator: "<=", + Value: 2, + }, + PropertyFilter{ + Path: "height", + Operator: ">", + Value: 7, + }, + }, + }, + ), + want: []map[string]interface{}{ + {"height": int64(1), "weight": int64(99), "updatedAt": int64(yesterdayTime)}, + {"height": int64(2), "weight": int64(98), "updatedAt": int64(yesterdayTime)}, + {"height": int64(8), "weight": int64(93), "updatedAt": int64(todayTime)}, + }, + orderBy: true, + }, + } { + if test.orderBy { + test.q = test.q.OrderBy("height", Asc) + } + gotDocs, err := test.q.Documents(ctx).GetAll() + if err != nil { + t.Errorf("#%d: %+v: %v", i, test.q, err) + continue + } + if len(gotDocs) != len(test.want) { + t.Errorf("#%d: (%q) %+v: got %d wants, want %d", i, test.desc, test.q, len(gotDocs), len(test.want)) + continue + } + for j, g := range gotDocs { + if got, want := g.Data(), test.want[j]; !testEqual(got, want) { + t.Errorf("#%d: %+v, #%d: got\n%+v\nwant\n%+v", i, test.q, j, got, want) + } + } + } +} + func TestIntegration_QueryDocuments(t *testing.T) { ctx := context.Background() coll := integrationColl(t) diff --git a/firestore/query.go b/firestore/query.go index 545cd3c1d2ab..8289fd9a3b3b 100644 --- a/firestore/query.go +++ b/firestore/query.go @@ -118,6 +118,7 @@ func (q Query) SelectPaths(fieldPaths ...FieldPath) Query { // fields, and must not contain any of the runes "˜*/[]". // The op argument must be one of "==", "!=", "<", "<=", ">", ">=", // "array-contains", "array-contains-any", "in" or "not-in". +// WARNING: Using WhereEntity with Simple and Composite filters is recommended. func (q Query) Where(path, op string, value interface{}) Query { fp, err := parseDotSeparatedString(path) if err != nil { @@ -131,8 +132,23 @@ func (q Query) Where(path, op string, value interface{}) Query { // A Query can have multiple filters. // The op argument must be one of "==", "!=", "<", "<=", ">", ">=", // "array-contains", "array-contains-any", "in" or "not-in". +// WARNING: Using WhereEntity with Simple and Composite filters is recommended. func (q Query) WherePath(fp FieldPath, op string, value interface{}) Query { - proto, err := filter{fp, op, value}.toProto() + return q.WhereEntity(PropertyPathFilter{ + Path: fp, + Operator: op, + Value: value, + }) +} + +// WhereEntity returns a query with provided filter. +// +// EntityFilter can be a simple filter or a composite filter +// PropertyFilter and PropertyPathFilter are supported simple filters +// AndFilter and OrFilter are supported composite filters +// Entity filters in multiple calls are joined together by AND +func (q Query) WhereEntity(ef EntityFilter) Query { + proto, err := ef.toProto() if err != nil { q.err = err return q @@ -464,7 +480,9 @@ func (q Query) toProto() (*pb.StructuredQuery, error) { Op: pb.StructuredQuery_CompositeFilter_AND, } p.Where = &pb.StructuredQuery_Filter{ - FilterType: &pb.StructuredQuery_Filter_CompositeFilter{cf}, + FilterType: &pb.StructuredQuery_Filter_CompositeFilter{ + CompositeFilter: cf, + }, } cf.Filters = append(cf.Filters, q.filters...) } @@ -643,21 +661,143 @@ func (q Query) compareFunc() func(d1, d2 *DocumentSnapshot) (int, error) { } } -type filter struct { - fieldPath FieldPath - op string - value interface{} +// EntityFilter represents a Firestore filter. +type EntityFilter interface { + toProto() (*pb.StructuredQuery_Filter, error) +} + +// CompositeFilter represents a composite Firestore filter. +type CompositeFilter interface { + EntityFilter + isCompositeFilter() +} + +// OrFilter represents a union of two or more filters. +type OrFilter struct { + Filters []EntityFilter +} + +func (OrFilter) isCompositeFilter() {} + +func (f OrFilter) toProto() (*pb.StructuredQuery_Filter, error) { + var pbFilters []*pb.StructuredQuery_Filter + + for _, filter := range f.Filters { + pbFilter, err := filter.toProto() + if err != nil { + return nil, err + } + pbFilters = append(pbFilters, pbFilter) + } + + cf := &pb.StructuredQuery_CompositeFilter{ + Op: pb.StructuredQuery_CompositeFilter_OR, + } + cf.Filters = append(cf.Filters, pbFilters...) + + return &pb.StructuredQuery_Filter{ + FilterType: &pb.StructuredQuery_Filter_CompositeFilter{ + CompositeFilter: cf, + }, + }, nil + +} + +// AndFilter represents the intersection of two or more filters. +type AndFilter struct { + Filters []EntityFilter } -func (f filter) toProto() (*pb.StructuredQuery_Filter, error) { - if err := f.fieldPath.validate(); err != nil { +func (AndFilter) isCompositeFilter() {} + +func (f AndFilter) toProto() (*pb.StructuredQuery_Filter, error) { + var pbFilters []*pb.StructuredQuery_Filter + + for _, filter := range f.Filters { + pbFilter, err := filter.toProto() + if err != nil { + return nil, err + } + pbFilters = append(pbFilters, pbFilter) + } + + cf := &pb.StructuredQuery_CompositeFilter{ + Op: pb.StructuredQuery_CompositeFilter_AND, + } + cf.Filters = append(cf.Filters, pbFilters...) + + return &pb.StructuredQuery_Filter{ + FilterType: &pb.StructuredQuery_Filter_CompositeFilter{ + CompositeFilter: cf, + }, + }, nil + +} + +// SimpleFilter represents a simple Firestore filter. +type SimpleFilter interface { + EntityFilter + isSimpleFilter() +} + +// PropertyFilter represents a filter on single property. +// +// Path can be a single field or a dot-separated sequence of fields +// denoting property path, and must not contain any of the runes "˜*/[]". +// Operator must be one of "==", "!=", "<", "<=", ">", ">=", +// "array-contains", "array-contains-any", "in" or "not-in". +type PropertyFilter struct { + Path string + Operator string + Value interface{} +} + +func (PropertyFilter) isSimpleFilter() {} + +func (f PropertyFilter) toPropertyPathFilter() (PropertyPathFilter, error) { + fp, err := parseDotSeparatedString(f.Path) + if err != nil { + return PropertyPathFilter{}, err + } + + ppf := PropertyPathFilter{ + Path: fp, + Operator: f.Operator, + Value: f.Value, + } + return ppf, nil +} + +func (f PropertyFilter) toProto() (*pb.StructuredQuery_Filter, error) { + ppf, err := f.toPropertyPathFilter() + if err != nil { + return nil, err + } + return ppf.toProto() +} + +// PropertyPathFilter represents a filter on single property. +// +// Path can be an array of fields denoting property path. +// Operator must be one of "==", "!=", "<", "<=", ">", ">=", +// "array-contains", "array-contains-any", "in" or "not-in". +type PropertyPathFilter struct { + Path FieldPath + Operator string + Value interface{} +} + +func (PropertyPathFilter) isSimpleFilter() {} + +func (f PropertyPathFilter) toProto() (*pb.StructuredQuery_Filter, error) { + if err := f.Path.validate(); err != nil { return nil, err } - if uop, ok := unaryOpFor(f.value); ok { - if f.op != "==" { - return nil, fmt.Errorf("firestore: must use '==' when comparing %v", f.value) + if uop, ok := unaryOpFor(f.Value); ok { + if f.Operator != "==" { + return nil, fmt.Errorf("firestore: must use '==' when comparing %v", f.Value) } - ref, err := fref(f.fieldPath) + ref, err := fref(f.Path) if err != nil { return nil, err } @@ -673,7 +813,7 @@ func (f filter) toProto() (*pb.StructuredQuery_Filter, error) { }, nil } var op pb.StructuredQuery_FieldFilter_Operator - switch f.op { + switch f.Operator { case "<": op = pb.StructuredQuery_FieldFilter_LESS_THAN case "<=": @@ -695,16 +835,16 @@ func (f filter) toProto() (*pb.StructuredQuery_Filter, error) { case "array-contains-any": op = pb.StructuredQuery_FieldFilter_ARRAY_CONTAINS_ANY default: - return nil, fmt.Errorf("firestore: invalid operator %q", f.op) + return nil, fmt.Errorf("firestore: invalid operator %q", f.Operator) } - val, sawTransform, err := toProtoValue(reflect.ValueOf(f.value)) + val, sawTransform, err := toProtoValue(reflect.ValueOf(f.Value)) if err != nil { return nil, err } if sawTransform { return nil, errors.New("firestore: transforms disallowed in query value") } - ref, err := fref(f.fieldPath) + ref, err := fref(f.Path) if err != nil { return nil, err } diff --git a/firestore/query_test.go b/firestore/query_test.go index ae47b08e9eac..0bdd56fc369d 100644 --- a/firestore/query_test.go +++ b/firestore/query_test.go @@ -31,12 +31,12 @@ import ( func TestFilterToProto(t *testing.T) { for _, test := range []struct { - in filter + in EntityFilter want *pb.StructuredQuery_Filter }{ { - filter{[]string{"a"}, ">", 1}, - &pb.StructuredQuery_Filter{FilterType: &pb.StructuredQuery_Filter_FieldFilter{ + in: PropertyFilter{"a", ">", 1}, + want: &pb.StructuredQuery_Filter{FilterType: &pb.StructuredQuery_Filter_FieldFilter{ FieldFilter: &pb.StructuredQuery_FieldFilter{ Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"}, Op: pb.StructuredQuery_FieldFilter_GREATER_THAN, @@ -45,8 +45,8 @@ func TestFilterToProto(t *testing.T) { }}, }, { - filter{[]string{"a"}, "==", nil}, - &pb.StructuredQuery_Filter{FilterType: &pb.StructuredQuery_Filter_UnaryFilter{ + in: PropertyFilter{"a", "==", nil}, + want: &pb.StructuredQuery_Filter{FilterType: &pb.StructuredQuery_Filter_UnaryFilter{ UnaryFilter: &pb.StructuredQuery_UnaryFilter{ OperandType: &pb.StructuredQuery_UnaryFilter_Field{ Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"}, @@ -56,8 +56,8 @@ func TestFilterToProto(t *testing.T) { }}, }, { - filter{[]string{"a"}, "==", math.NaN()}, - &pb.StructuredQuery_Filter{FilterType: &pb.StructuredQuery_Filter_UnaryFilter{ + in: PropertyFilter{"a", "==", math.NaN()}, + want: &pb.StructuredQuery_Filter{FilterType: &pb.StructuredQuery_Filter_UnaryFilter{ UnaryFilter: &pb.StructuredQuery_UnaryFilter{ OperandType: &pb.StructuredQuery_UnaryFilter_Field{ Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"}, @@ -66,6 +66,166 @@ func TestFilterToProto(t *testing.T) { }, }}, }, + { + in: PropertyPathFilter{[]string{"a"}, ">", 1}, + want: &pb.StructuredQuery_Filter{FilterType: &pb.StructuredQuery_Filter_FieldFilter{ + FieldFilter: &pb.StructuredQuery_FieldFilter{ + Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"}, + Op: pb.StructuredQuery_FieldFilter_GREATER_THAN, + Value: intval(1), + }, + }}, + }, + { + in: PropertyPathFilter{[]string{"a"}, "==", nil}, + want: &pb.StructuredQuery_Filter{FilterType: &pb.StructuredQuery_Filter_UnaryFilter{ + UnaryFilter: &pb.StructuredQuery_UnaryFilter{ + OperandType: &pb.StructuredQuery_UnaryFilter_Field{ + Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"}, + }, + Op: pb.StructuredQuery_UnaryFilter_IS_NULL, + }, + }}, + }, + { + in: PropertyPathFilter{[]string{"a"}, "==", math.NaN()}, + want: &pb.StructuredQuery_Filter{FilterType: &pb.StructuredQuery_Filter_UnaryFilter{ + UnaryFilter: &pb.StructuredQuery_UnaryFilter{ + OperandType: &pb.StructuredQuery_UnaryFilter_Field{ + Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"}, + }, + Op: pb.StructuredQuery_UnaryFilter_IS_NAN, + }, + }}, + }, + { + in: OrFilter{ + Filters: []EntityFilter{ + PropertyPathFilter{[]string{"a"}, ">", 5}, + PropertyFilter{"a", "<=", 2}, + }, + }, + want: &pb.StructuredQuery_Filter{ + FilterType: &pb.StructuredQuery_Filter_CompositeFilter{ + CompositeFilter: &pb.StructuredQuery_CompositeFilter{ + Op: pb.StructuredQuery_CompositeFilter_OR, + Filters: []*pb.StructuredQuery_Filter{ + { + FilterType: &pb.StructuredQuery_Filter_FieldFilter{ + FieldFilter: &pb.StructuredQuery_FieldFilter{ + Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"}, + Op: pb.StructuredQuery_FieldFilter_GREATER_THAN, + Value: intval(5), + }, + }, + }, + { + FilterType: &pb.StructuredQuery_Filter_FieldFilter{ + FieldFilter: &pb.StructuredQuery_FieldFilter{ + Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"}, + Op: pb.StructuredQuery_FieldFilter_LESS_THAN_OR_EQUAL, + Value: intval(2), + }, + }, + }, + }, + }, + }, + }, + }, + { + in: AndFilter{ + Filters: []EntityFilter{ + PropertyPathFilter{[]string{"a"}, ">", 5}, + PropertyFilter{"a", "<=", 10}, + }, + }, + want: &pb.StructuredQuery_Filter{ + FilterType: &pb.StructuredQuery_Filter_CompositeFilter{ + CompositeFilter: &pb.StructuredQuery_CompositeFilter{ + Op: pb.StructuredQuery_CompositeFilter_AND, + Filters: []*pb.StructuredQuery_Filter{ + { + FilterType: &pb.StructuredQuery_Filter_FieldFilter{ + FieldFilter: &pb.StructuredQuery_FieldFilter{ + Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"}, + Op: pb.StructuredQuery_FieldFilter_GREATER_THAN, + Value: intval(5), + }, + }, + }, + { + FilterType: &pb.StructuredQuery_Filter_FieldFilter{ + FieldFilter: &pb.StructuredQuery_FieldFilter{ + Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"}, + Op: pb.StructuredQuery_FieldFilter_LESS_THAN_OR_EQUAL, + Value: intval(10), + }, + }, + }, + }, + }, + }, + }, + }, + { + in: OrFilter{ + Filters: []EntityFilter{ + PropertyPathFilter{[]string{"b"}, "==", 15}, + AndFilter{ + Filters: []EntityFilter{ + PropertyPathFilter{[]string{"a"}, ">", 5}, + PropertyFilter{"a", "<=", 12}, + }, + }, + }, + }, + want: &pb.StructuredQuery_Filter{ + FilterType: &pb.StructuredQuery_Filter_CompositeFilter{ + CompositeFilter: &pb.StructuredQuery_CompositeFilter{ + Op: pb.StructuredQuery_CompositeFilter_OR, + Filters: []*pb.StructuredQuery_Filter{ + { + FilterType: &pb.StructuredQuery_Filter_FieldFilter{ + FieldFilter: &pb.StructuredQuery_FieldFilter{ + Field: &pb.StructuredQuery_FieldReference{FieldPath: "b"}, + Op: pb.StructuredQuery_FieldFilter_EQUAL, + Value: intval(15), + }, + }, + }, + { + FilterType: &pb.StructuredQuery_Filter_CompositeFilter{ + CompositeFilter: &pb.StructuredQuery_CompositeFilter{ + Op: pb.StructuredQuery_CompositeFilter_AND, + Filters: []*pb.StructuredQuery_Filter{ + { + FilterType: &pb.StructuredQuery_Filter_FieldFilter{ + FieldFilter: &pb.StructuredQuery_FieldFilter{ + Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"}, + Op: pb.StructuredQuery_FieldFilter_GREATER_THAN, + Value: intval(5), + }, + }, + }, + { + FilterType: &pb.StructuredQuery_Filter_FieldFilter{ + FieldFilter: &pb.StructuredQuery_FieldFilter{ + Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"}, + Op: pb.StructuredQuery_FieldFilter_LESS_THAN_OR_EQUAL, + Value: intval(12), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, } { got, err := test.in.toProto() if err != nil { @@ -86,7 +246,7 @@ type toProtoScenario struct { // Creates protos used to test toProto, FromProto, ToProto funcs. func createTestScenarios(t *testing.T) []toProtoScenario { filtr := func(path []string, op string, val interface{}) *pb.StructuredQuery_Filter { - f, err := filter{path, op, val}.toProto() + f, err := PropertyPathFilter{path, op, val}.toProto() if err != nil { t.Fatal(err) } @@ -191,6 +351,54 @@ func createTestScenarios(t *testing.T) []toProtoScenario { }, }, }, + { + desc: `q.WhereEntity(AndFilter({"a", ">", 5}, {"b", "<", "foo"}))`, + in: q.WhereEntity( + AndFilter{ + Filters: []EntityFilter{ + PropertyFilter{ + Path: "a", + Operator: ">", + Value: 5, + }, + PropertyFilter{ + Path: "b", + Operator: "<", + Value: "foo", + }, + }, + }, + ), + want: &pb.StructuredQuery{ + Where: &pb.StructuredQuery_Filter{ + FilterType: &pb.StructuredQuery_Filter_CompositeFilter{ + CompositeFilter: &pb.StructuredQuery_CompositeFilter{ + Op: pb.StructuredQuery_CompositeFilter_AND, + Filters: []*pb.StructuredQuery_Filter{ + { + FilterType: &pb.StructuredQuery_Filter_FieldFilter{ + FieldFilter: &pb.StructuredQuery_FieldFilter{ + Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"}, + Op: pb.StructuredQuery_FieldFilter_GREATER_THAN, + Value: intval(5), + }, + }, + }, + { + FilterType: &pb.StructuredQuery_Filter_FieldFilter{ + FieldFilter: &pb.StructuredQuery_FieldFilter{ + Field: &pb.StructuredQuery_FieldReference{FieldPath: "b"}, + Op: pb.StructuredQuery_FieldFilter_LESS_THAN, + Value: strval("foo"), + }, + }, + }, + }, + }, + }, + }, + }, + }, { desc: ` q.WherePath([]string{"/", "*"}, ">", 5)`, in: q.WherePath([]string{"/", "*"}, ">", 5), @@ -510,7 +718,23 @@ func TestQueryToProtoErrors(t *testing.T) { q.Where("x", "<>", 1), // invalid operator q.Where("~", ">", 1), // invalid path q.WherePath([]string{"*", ""}, ">", 1), // invalid path - q.StartAt(1), // no OrderBy + q.WhereEntity( // invalid nested filters + AndFilter{ + Filters: []EntityFilter{ + PropertyFilter{ + Path: "x", + Operator: "<>", + Value: 1, + }, + PropertyFilter{ + Path: "~", + Operator: ">", + Value: 1, + }, + }, + }, + ), + q.StartAt(1), // no OrderBy q.StartAt(2).OrderBy("x", Asc).OrderBy("y", Desc), // wrong # OrderBy q.Select("*"), // invalid path q.SelectPaths([]string{"/", "", "~"}), // invalid path diff --git a/firestore/util_test.go b/firestore/util_test.go index 501b388dd0af..322da257ca33 100644 --- a/firestore/util_test.go +++ b/firestore/util_test.go @@ -50,7 +50,7 @@ func mustTimestampProto(t time.Time) *tspb.Timestamp { var cmpOpts = []cmp.Option{ cmp.AllowUnexported(DocumentSnapshot{}, - Query{}, filter{}, order{}, fpv{}, DocumentRef{}, CollectionRef{}, Query{}), + Query{}, OrFilter{}, AndFilter{}, PropertyPathFilter{}, PropertyFilter{}, order{}, fpv{}, DocumentRef{}, CollectionRef{}, Query{}), cmpopts.IgnoreTypes(Client{}, &Client{}), cmp.Comparer(func(*readSettings, *readSettings) bool { return true // Don't try to compare two readSettings pointer types